SwiftUI · · 3 min read

How to Use ScrollViewReader to Perform Programmatic Scrolling

How to Use ScrollViewReader to Perform Programmatic Scrolling

ScrollViewReader is one of my favorite new features in the new version of SwiftUI. Before the release of iOS 14, it’s not easy to control the scrolling position of the built-in ScrollView. If you want the scroll view to scroll to a particular location, you have to figure out your own solution.

With ScrollViewReader, you can programmatically make the scroll view to scroll to a specific location with just a few lines of code. In this tutorial, we will look into this new view component and see how you can apply it to your app.

Creating a Horizontal ScrollView

To demonstrate the usage of ScrollViewReader, let’s start with a simple demo and take a look at the following piece of code:

struct ContentView: View {

    let photos = [ "bigben", "bridge", "canal", "effieltower", "flatiron", "oia" ]

    var body: some View {
        GeometryReader { geometry in
            ScrollView(.horizontal) {
                HStack(alignment: .center) {
                    ForEach(photos.indices) { index in
                        Image(photos[index])
                            .resizable()
                            .scaledToFill()
                            .frame(width: geometry.size.width - 50)
                            .cornerRadius(25)
                            .padding(.horizontal, 25)
                    }
                }
            }
        }
    }
}

We’ve built a horizontal scroll view to display a group of photos. If you run the code on a simulator or in the preview pane, you can swipe horizontally to scroll through the photos.

scrollviewreader-app-demo

Now what if we want to add a few buttons for users to navigate back and forth between photos? In other words, how can you make the scroll view scroll to a particular image?

Using ScrollViewReader

The release of ScrollViewReader is the answer to this problem. The usage of this new view component is very simple. You first give each element in the scroll view a unique ID. For the demo, you can attach an .id modifier to the Image view and set its value to the index of the photo:

Image(photos[index])
    .resizable()
    .scaledToFill()
    .frame(width: geometry.size.width - 50)
    .cornerRadius(25)
    .padding(.horizontal, 25)
    .id(index)

Next, you wrap the scroll view with a ScrollViewReader. The scroll view reader’s content view builder receives a ScrollViewProxy instance:

GeometryReader { geometry in
    VStack {
        ScrollViewReader { scrollView in
            ScrollView(.horizontal) {
                HStack(alignment: .center) {
                    ForEach(photos.indices) { index in
                        Image(photos[index])
                            .resizable()
                            .scaledToFill()
                            .frame(width: geometry.size.width - 50)
                            .cornerRadius(25)
                            .padding(.horizontal, 25)
                            .id(index)
                    }
                }
            }

            // Navigation Buttons
            HStack {
                // To be complete
            }
            .frame(height: 70)
        }
    }
}

Once you have the proxy, you can call the scrollTo function to scroll to a particular index. For example, the following line of code asks the scroll view to scroll to the last photo:

scrollView.scrollTo(photos.count - 1)

To complete the demo, you can declare a state variable to keep track of the current photo index:

@State private var currentIndex = 0

And then you can fill in the code in HStack like this:

HStack {
    Button(action: {
        withAnimation {
            scrollView.scrollTo(0)
        }
    }) {
        Image(systemName: "backward.end.fill")
            .font(.system(size: 50))
            .foregroundColor(.black)
    }

    Button(action: {
        withAnimation {
            currentIndex = (currentIndex == 0) ? currentIndex : currentIndex - 1
            scrollView.scrollTo(currentIndex)
        }
    }) {
        Image(systemName: "arrowtriangle.backward.circle")
            .font(.system(size: 50))
            .foregroundColor(.black)
    }

    Button(action: {
        withAnimation {
            currentIndex = (currentIndex == photos.count - 1) ? currentIndex : currentIndex + 1
            scrollView.scrollTo(currentIndex)
        }
    }) {
        Image(systemName: "arrowtriangle.forward.circle")
            .font(.system(size: 50))
            .foregroundColor(.black)
    }

    Button(action: {
        withAnimation {
            scrollView.scrollTo(photos.count - 1)
        }
    }) {
        Image(systemName: "forward.end.fill")
            .font(.system(size: 50))
            .foregroundColor(.black)
    }
}
.frame(height: 70)

For each button, we call the scrollTo function to scroll to the particular photo. By wrapping the code using withAnimation, the app will present a nice scrolling animation.

scrollviewreader-perform-scrolling

Working with List

Not only can you wrap a ScrollView with a ScrollViewReader, it also works with List too. Let’s say, you convert the app to use a list instead of a horizontal scroll view like this:

ScrollViewReader { scrollView in
    List {
        ForEach(photos.indices) { index in
            Image(photos[index])
                .resizable()
                .scaledToFill()
                .cornerRadius(25)
                .id(index)
        }
    }

    .
    .
    .
}

You can still apply the scrollTo function to ask the list to scroll to a specific element.

Summary

ScrollViewReader is a great addition to the SwiftUI framework. Without developing your own solution, you can now easily instruct any scroll views to scroll to a particular location. I hope you also find this new component useful and enjoy reading the tutorial. If you want to dive deeper into SwiftUI and learn other techniques, don’t forget to check out our Mastering SwiftUI book.

Read next