SwiftUI custom ScrollView with snap interval

Due that fact that SwiftUI has poor ScrollView customization and doesn’t support snap interval from the box I decided to implement my custom component to support such behaviour.

Example

enter image description here

enter image description here

Here my code

I know, there are many hard coded values that should be replaced with dynamically calculated values. But for my debug purposes is enough.

I would like to ask if it possible to simplify my code? Or is there any better approach of creating such component.

The thing that confuses me the most is lastTimePoint. The variable which stores the last shift of content. It needs to prevent content jumps on scroll. I will leave more comments in a code to help you understand what’s going on.

struct CustomScrollView<Content: View>: View {
  var height: CGFloat
  var content: () -> Content
  
  @State private var offset = CGFloat.zero
  @State private var lastTimePoint = CGFloat.zero // lastTimePoint the same as offset. I use it to populate value.translation.height and value.predictedEndTranslation.height to prevent content jumps. I want to remove it if it possible.
  
  var body: some View {
      VStack(alignment: .leading, spacing: 0) {
        content()
          .offset(y: offset) // Scrolls content depending on the this value
          .contentShape(Rectangle())
          .gesture(
            DragGesture(minimumDistance: 0)
              .onChanged(handleDragGestureChange)
              .onEnded(handleDragGestureEnded)
          )
      }
      .frame(height: height, alignment: .top) // clip the content to make content scrollable. height is equal to 250
      .clipped()
  }
  
  private func handleDragGestureChange(value: DragGesture.Value) { // Updates offset value
    offset = value.translation.height + lastTimePoint // If I remove lastTimePoint content will jump to the top of the list (if user started to scroll from any other place)
  }
  
  private func handleDragGestureEnded(value: DragGesture.Value) { // I use this function to calculate closest row where offset should stop.
    let predictedY = value.predictedEndTranslation.height + lastTimePoint // Again add lastTimePoint to prevent content jump
    
    withAnimation(.spring()) { // Animates list scrolling after user stops interacting
      if predictedY > 0 { // If list is going beyond his boundaries stop at the most top visible point (first element)
        offset = 0
      } else if predictedY < -1750 { // 1750 is the size of the content inside (red rectangles). I calculated it by 40 items * 50 item height = 2000 - 250 = 1750 where 1750 total height of all items within 250 height clipped scrollable component 
        offset = -1750
      } else { // In this block I calculate the closest row where scrollable view should stop. 

        // My rows heights
        // 0
        // -50
        // -100
        // -150
        // -200
        // ...
        // -1750
        // 
        // If offset position is 120 (which is closer to 100 than to 150) I calculate it like this
        // -120 % 50 = remainder -20 
        // remainder -20 is less then -25 go to the bottom, otherwise to to the top

        let remainder = predictedY.truncatingRemainder(dividingBy:  50)



        if remainder < -25 {
          offset = predictedY - (50 + remainder)
        } else {
          offset = predictedY - remainder
        }
      }
    }
    
    lastTimePoint = offset
  }
}

struct CustomScrollView_Previews: PreviewProvider {
    static var previews: some View {
        CustomScrollView(
          height: 250
        ) {
          Text("View")
        }
    }
}