Пользовательский ScrollView SwiftUI с интервалом привязки

Из-за того, что SwiftUI плохо ScrollView настройки и не поддерживает интервал привязки из коробки. Я решил реализовать свой собственный компонент для поддержки такого поведения.

пример

введите описание изображения здесь

введите описание изображения здесь

Вот мой код

Я знаю, что есть много жестко закодированных значений, которые следует заменить динамически вычисляемыми значениями. Но для моих отладочных целей этого достаточно.

Я хотел бы спросить, можно ли упростить мой код? Или есть лучший подход к созданию такого компонента.

Больше всего меня смущает lastTimePoint. Переменная, в которой хранится последний сдвиг содержимого. Это необходимо для предотвращения скачков содержимого при прокрутке. Я оставлю больше комментариев в коде, чтобы помочь вам понять, что происходит.

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")
        }
    }
}

0

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *