I'm currently working on a SwiftUI project where I need to create a custom vertical ScrollView. The requirement is for the cells within this ScrollView to snap to the center of the view when the user stops scrolling. I understand that SwiftUI's ScrollView provides some level of customization through the .scrollTargetBehavior(_:) modifier, but the documentation and examples I've found don't quite cover this specific case.
I've tried using the basic scrollTargetBehavior .viewAligned, but the snapping behavior doesn't include the snapping effect I'm looking for. I'm aware that UIKit provides more granular control over scrolling behavior with UICollectionView and custom layout attributes, but I'm aiming to achieve this purely within the SwiftUI.
Any help would be highly appreciated.
Cheers!
For the sake of a working example, I'm going to use many instances of this as the content of the
ScrollView:This draws a sort of playing card with a cross in the center. The cross will make it easy to see whether the
ScrollViewcenters a card when it stops scrolling.Let's start with a basic
ScrollViewsetup containing the full deck of cards:It looks like this:
The green line is at the vertical center of the
ScrollView. We can tell that a card is centered if the card's cross lines up with the green line.To make the
ScrollViewstop scrolling with a centered card, we need to write a custom implementation ofScrollTargetBehavior. By reading the documentation (and in particular the documentation ofScrollTargetBehaviorContextandScrollTarget), we can infer that our customScrollTargetBehaviorneeds access to the frames of the card views, in theScrollViews coordinate space.To collect those frames, we need to use SwiftUI's “preference” system. First, we need a type to collect the card frames:
Next, we need a custom implementation of the
PreferenceKeyprotocol. We might as well use theCardFramestype as the key:We need to add a
@Stateproperty to store the collected frames:We also need to define a
NamedCoordinateSpacefor theScrollView:Next we need to apply that coordinate space to the content of the
ScrollView, by adding acoordinateSpacemodifier to theLazyVStack:To read the frame of a
Cardand set the preference, we use a common SwiftUI pattern: add abackgroundcontaining aGeometryReadercontaining aColor.clearwith apreferencemodifier:Now we can read out the
CardFramespreference and store it in the@Stateproperty, by using theonPreferenceChangemodifier:That is all the code to collect the card frames and make them available in the
cardFramesproperty.Now we're ready to write a custom
ScrollTargetBehavior. Our custom behavior adjusts theScrollTargetso that its midpoint is the midpoint of the nearest card:Finally, we use the
scrollTargetBehaviormodifier to apply our custom behavior to theScrollView:I noticed that, when scrolling back up and landing on the 3♥︎ card, it's not quite centered. I think that's a SwiftUI bug.
Here's the final
ContentViewwith all the additions: