Search code examples
swiftuiuicollectionviewuiscrollviewscrollviewscrollviewreader

Implementing SwiftUI ScrollView snap-to-placement behavior (perhaps using a "scrollViewWillEndDragging" equivalent)?


There is a common way of handling scrolling with UICollectionView so that when the user stops scrolling it gradually slows down and stops with the nearest item in the collection view lined up with the edge of the collection view's bounds.

This differs from page views in that the user can quickly scroll past several items in the connection view with a single swipe, and multiple items are displayed onscreen at once.

You can see this in Apple's own apps: TV, Music, Books, and Podcasts all exhibit this behavior when browsing horizontal lists in each respective app.

With UICollectionView (or UIScrollView, but it's more typical in collection views) this is typically implemented using the scrollViewWillEndDragging(scrollView:velocity:targetContentOffset:) method to detect when the user has stopped scrolling and supplying it with an updated target offset to scroll to.

The top answer (by rob mayoff) to the following question describes this in some detail:

How to snap horizontal paging to multi-row collection view like App Store?

My question: How do I do this using SwiftUI's ScrollView?

I have a horizontal ScrollView enclosing an HGrid with a set of items in it. I would like to implement a horizontal browser similar to that in Apple's apps. I can do it using a UICollectionView, as described above, but would much prefer to do it "natively" in SwiftUI without wrapping a UICollectionView.

I would expect this to be possible using ScrollViewReader and scrollTo(_:anchor:) somehow, but it doesn't seem that it has anything to addresses this need.

Is this behavior possible to implement natively using SwiftUI, and - if so - how?


Solution

  • I was never able to find a satisfactory option for doing this in a clean way with iOS 16 or earlier. I was resigned to just wrap UIKit's UICollectionView but decided to wait until WWDC 2023 to see what Apple might have in store.

    I'm glad I did, as ScrollView got some significant improvements with iOS 17 - including the ability to define where and how SwiftUI's ScrollView handles elements coming to rest.

    Namely, this is done using the new scrollTargetBehavior modifier (docs) that takes as an argument a ScrollTargetBehavior. For my purposes .viewAligned provided what I needed out of the box, but there is also a .paging option that some might find useful.

    You can also define your own ScrollTargetBehavior which, I believe, opens this up to all sorts of future uses.

    For the .viewAligned behavior you will need to define the parent view (i.e. HStack) whose constituent views the ScrollView is to align to using the .scrollTargetLayout() modifier. You can also do this with individual views using the .scrollTarget() modifier. You can read the documentation for all this, here.

    The WWDC 2023 session "Beyond scroll views" goes into detail about what's new with ScrollView. You can watch it here.

    Anyhow, I tested this in my own app and it works great. I'm content to wait until iOS 17 is released (and simply not do view alignment at all in iOS 16) for my app, so I think this solves my issue.