Background
At WWDC 2023, Apple introduced a new API for ScrollViews that lets us neatly snap views into position with little effort. The following code example snaps the Rectangles into position when the user lifts the finger after a scroll (drag) gesture.
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack(spacing: 8) {
ForEach(1..<100) { number in
Rectangle()
.fill(.mint)
.frame(height: 200)
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.viewAligned)
}
}
Question
Now what if I want to only snap to every second Rectangle and scroll over the others? Or more generally speaking, how can I control exactly which items (child views) the ScrollView should snap to and which one it should ignore for snapping?
It seems like a very trivial thing, but I couldn't figure out how to accomplish this.
A Hint?
The documentation for the scrollTargetLayout(isEnabled:) modifier states:
Scroll target layouts act as a convenience for applying a
View/scrollTarget(isEnabled:)modifier to each views in the layout.
This tells me that there is another modifier called scrollTarget(isEnabled:) that I can attach to individual items to granularly enable or disable snapping. However, this modifier doesn't seem to exist. ♂️
Is the documentation simply wrong or did I misunderstand it? How can I make this work (without falling back to UIKit)?
It turns out it's possible to that with the new iOS 17 APIs for ScrollViews. For that you can use the new
.scrollPosition(id: Hashable)modifier on a scroll view. I have been a bit of fun with it. Here's what I have accomplished so far:Here's the code I've used for it. I'm using an itemCount variable to generate numbers from 1 to 100. As for the non-programatic part I've used the onChange modifier and handled the case there. I've had to use a flag that I've called "onChangedExecuted" because otherwise the event would fire more times then we need.
Modifying the activeID integer number the scroll view will automatically navigate to the element at that index. In this version of the code I've used the new
.containerRelativeFrame(.horizontal)to center the activeID text in the middle, not sure you wanted it that way but it seemed the easiest solution to me. Now you can choose with a picker how many items should be "skipped". You can apply the same principle as per your needs. Hope I was able to help you, and let me know!