Problem
How can I modify the scroll target of a scrollView? I am looking for kind of a replacement for the "classic" scrollView delegate method
override func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
...where we can modfify the targeted scrollView.contentOffset
via targetContentOffset.pointee
for instance to create a custom paging behaviour.
Or in other words: I do want to create a paging effect in a (horizontal) scrollView.
What I have tried ie. is something like this:
ScrollView(.horizontal, showsIndicators: true, content: { HStack(alignment: VerticalAlignment.top, spacing: 0, content: { card(title: "1") card(title: "2") card(title: "3") card(title: "4") }) }) // 3. .content.offset(x: self.dragState.isDragging == true ? self.originalOffset : self.modifiedOffset, y: 0) // 4. .animation(self.dragState.isDragging == true ? nil : Animation.spring()) // 5. .gesture(horizontalDragGest)
Attempt
This is what I tried (besides a custom scrollView approach):
A scrollView has a content area larger then screen space to enable scrolling at all.
I created a DragGesture()
to detect if there is a drag going on. In the .onChanged and .onEnded closures I modified my @State
values to create a desired scrollTarget.
Conditionally fed in both the original unchanged and the new modified values into the .content.offset(x: y:) modifier - depending on the dragState as a replacement for missing scrollDelegate methods.
Added animation acting conditionally only when drag has ended.
Attached the gesture to the scrollView.
Long story short. It doesn't work. I hope I got across what my problem is.
Any solutions out there? Looking forward to any input. Thanks!
I have managed to achieve a paging behaviour with a @Binding
index. The solution might look dirty, I'll explain my workarounds.
The first thing I got wrong, was to get alignment to .leading
instead of the default .center
, otherwise the offset works unusual. Then I combined the binding and a local offset state. This kinda goes against the "Single source of truth" principle, but otherwise I had no idea how to handle external index changes and modify my offset.
So, my code is the following
struct SwiftUIPagerView<Content: View & Identifiable>: View { @Binding var index: Int @State private var offset: CGFloat = 0 @State private var isGestureActive: Bool = false // 1 var pages: [Content] var body: some View { GeometryReader { geometry in ScrollView(.horizontal, showsIndicators: false) { HStack(alignment: .center, spacing: 0) { ForEach(self.pages) { page in page .frame(width: geometry.size.width, height: nil) } } } // 2 .content.offset(x: self.isGestureActive ? self.offset : -geometry.size.width * CGFloat(self.index)) // 3 .frame(width: geometry.size.width, height: nil, alignment: .leading) .gesture(DragGesture().onChanged({ value in // 4 self.isGestureActive = true // 5 self.offset = value.translation.width + -geometry.size.width * CGFloat(self.index) }).onEnded({ value in if -value.predictedEndTranslation.width > geometry.size.width / 2, self.index < self.pages.endIndex - 1 { self.index += 1 } if value.predictedEndTranslation.width > geometry.size.width / 2, self.index > 0 { self.index -= 1 } // 6 withAnimation { self.offset = -geometry.size.width * CGFloat(self.index) } // 7 DispatchQueue.main.async { self.isGestureActive = false } })) } } }
.leading
is mandatory if you don't want to translate all offsets to center.I have tested it in the following context
struct WrapperView: View { @State var index: Int = 0 var body: some View { VStack { SwiftUIPagerView(index: $index, pages: (0..<4).map { index in TODOView(extraInfo: "\(index + 1)") }) Picker(selection: self.$index.animation(.easeInOut), label: Text("")) { ForEach(0..<4) { page in Text("\(page + 1)").tag(page) } } .pickerStyle(SegmentedPickerStyle()) .padding() } } }
where TODOView
is my custom view that indicates a view to implement.
I hope I get the question right, if not please specify which part should I focus on. Also I welcome any suggestions to remove the isGestureActive
state.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With