Из-за того, что 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")
}
}
}