How to Scroll to a Percentage in a ScrollView
Table of Contents
SwiftUI can make many tasks extremely easy, yet SwiftUi struggles to do other seemingly simple tasks. Today we will learn how to accomplish one of those tasks. We will create a ScrollView that can programmatically scroll to a specific location within the ScrollView. First let’s create a new type called ProgrammaticScrollView
.
struct ProgrammaticScrollView<Content: View>: View {
@Binding private var scrollID: Int?
let content: () -> Content
init(scrollID: Binding<Int?>, @ViewBuilder content: @escaping () -> Content) {
self._scrollID = scrollID
self.content = content
}
var body: some View {
ScrollView {
ForEach(1..<101) { num in
VStack {
Text("\(num)").frame(maxWidth: .infinity, alignment: .leading)
.id(num)
Spacer()
}
}
}
.scrollPosition(id: $scrollID, anchor: .top)
}
}
Here we’re using iOS 17’s new scrollPosition(id: anchor:) method. This method receives an id
of type Binding<(some Hashable)?>
and then scrolls to a child view with that id. As you can see there are 100 child views numbered 1 to 100, each with a corresponding id. Programmatically scrolling is now as simple as changing the value of our scrollID Binding!
Notice how the numbers are evenly spaced vertically. Now why don’t we hide those numbers from the user?
// ...
ScrollView {
content()
.padding(.horizontal)
.background {
VStack {
ForEach(1..<101) { num in
VStack {
Text("\(num)").frame(maxWidth: .infinity, alignment: .leading)
.id(num)
.opacity(0.0)
Spacer()
}
}
}
}
}
.scrollPosition(id: $scrollID, anchor: .top)
// ...
You might be thinking, why don’t we just use ScrollViewReader. Well, the techniques in this tutorial should be just as easy to implement using iOS 14’s ScrollViewReader
. It would just be slightly more complex since you would need to wrap your ScrollView
in a ScrollViewReader
and then give commands to a ScrollViewProxy
.
Some Quirks
scrollPosition(id: anchor:)
has some other benefits over ScrollViewReader
. The docs promise that the ScrollView will automatically update the Binding, thus giving you the freshest position of the ScrollView. The docs say:
As the scroll view scrolls, the binding will be updated with the identity of the leading-most / top-most view.
Unfortunately, seemingly due to bugs, it just doesn’t do that at all. In my testing, the Binding is just never updated by the ScrollView. But at least you can scroll programmatically.
Also, the docs say that you must use scrollTargetLayout()
. I don’t see why. I have found no difference in behavior with or without that method, so I’m just leaving it out of my view.
iOS 18’s New API
Now apparently, iOS 18 added yet another new method called scrollPosition(_: anchor:) which receives a new ScrollPosition
type. (I haven’t tried the new iOS 18 beta yet, so I don’t know if this actually works yet.)
In Practice
Now that have something workable, let’s take it for a spin.
struct ExampleView: View {
@State private var scrollPercentage: Int? = 1
@State private var picker = 34
var body: some View {
ProgrammaticScrollView(scrollID: $scrollPercentage)
.safeAreaInset(edge: .bottom) {
bottomBar
}
}
@ViewBuilder var bottomBar: some View {
VStack {
HStack {
Picker("Select a number", selection: $picker) {
ForEach(0..<100) { num in
Text("\(num)").id(num)
}
}
Button("Scroll to \(picker)%") { scrollPercentage }
}
LabeledContent("scrollPercentage", value: "\(scrollPercentage)") // useful for debugging
}
.padding(.horizontal)
.background(.thinMaterial, ignoresSafeAreaEdges: .bottom)
}
}
struct CircleButton<Background: ShapeStyle>: ButtonStyle {
let background: Background
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding()
.background(background, in: .circle)
}
}
Now we have a View where we can test programmatically scrolling our scroll view to any arbitrary position on our screen. This type of behavior would be extremely helpful for situations such as scrolling the transcript of a podcast, to current position of the audio while listening.
Today we learned how, with a little bit of ingenuity we can add powerful features to our UI. If you’d like to see a full code example, you can have a look at this gist.
If you like this work, please share it with others. Check back every week on Wednesdays for new posts.