I tried but failed to get a lab appointment for this issue, having submitted Feedback for it "ScrollView cannot be used to "snap" child views to a specific on-screen region" (FB7784831). Here's the content of the FB and the sample code:
We need to be able to replicate the UIKit-era behaviours of UIScrollViewDelegate where we could handle
scrollViewWillEndDragging and receive the anticipated scroll end point and amend it if necessary, to make items “snap” to align to a certain location on screen, rather than just permitting free scrolling.
This seems like it is still not possible in SwiftUI, if you e.g. envision creating something like the old iOS date picker which “snaps” the item nearest to a guide (e.g. center) when the user ceases scrolling.
We have tried various techniques, with the worst and most painful being using a totally custom View and custom offsetting of the content when a drag gesture onChange occurs, and projecting out the final offset when onEnded occurs. This does not feel correct on any platform as the animation inertia curves aren’t perfect and obviously has performance issues for large lists.
It feels like
ScrollViewReader gets us very close to being able to do this so I would love your input on how to achieve this very old capability we’ve had in UIKit since iOS 5!
Attached is a sample project that shows an attempt to get close to this using the new
ScrollViewReader.
The sample:
does not attempt to capture geometry of each scroll view child, for simplicity
attempts to capture a “did end dragging” equivalent by using a simultaneous drag gesture on each child view, but onEnded does not get called in current iOS 14 betas
does not attempt to scroll to the correct location because the event never triggers, and the sample does capture the geometry yet - and the scrollTo function on ScrollViewProxy does not seem to offer the required ability to either simply scroll to a specific Y offset (based on the Y offset of the child nearest to the predicted end location of the drag) or the ability to scroll item with a given ID to align a specific edge e.g. ID’s .top with a specific scroll offset in the scroll view. e.g. “align item “N” top to y = 500”.
If the above can be made to work that would be great.
However it seems a vastly preferable, if old fashioned solution, would be to have an:
Code Block .onScrollingWillEnd { offset -> CGPoint in … } |
…event modifier on ScrollView so that we could receive the anticipated scroll offset and return a modified version if we want to.
Even better would be something like:
Code Block ScrollView { |
… child views … |
} |
.onScrollingWillEnd { offset -> ScrollTarget in |
return ScrollTarget(id: findIDOfChildNearest(to: offset), |
anchor: .top, // Align the top of the view with the given ID |
toAnchor: mySnapPointAnchor) // to this anchor point which might be in another coordinate space |
} |