Scrolling Programmatically with scrollPosition in SwiftUI

It's often necessary to make a scroll view (or a list) scroll programmatically. As of iOS 18, SwiftUI offers the scrollPosition(_:) view modifier, which, along with one or two other modifiers, enable programmatic scrolling in just a few easy steps. Using it, scrolling can be towards a particular item, an edge, or just an arbitrary offset to the horizontal or vertical axis. Let's get to know it in the next few parts.

Note

To support system versions prior to 18, we still can use the ScrollViewReader container view, which is available as of iOS 14.

The scrollPosition modifier

To get started, suppose we have the following array that stores random integer numbers:

@State private var randomNumbers = [Int]()

A few random numbers are generated and assigned to that array when the view appears, while they are displayed using a scroll view and a ForEach container as shown next; note that there's a toolbar that remains empty at the time:

NavigationStack {
    ScrollView {
        ForEach(randomNumbers.indices, id: \.self) { index in
            VStack {
                HStack {
                    Text("\(randomNumbers[index])")
                    Spacer()
                }
                Divider()
            }
            .padding(.leading)
            .frame(height: 60)
        }
    }
    .onAppear {
        for _ in 0..<20 {
            randomNumbers.append(Int.random(in: 0...1000))
        }
    }
    .toolbar {
        ToolbarItemGroup(placement: .primaryAction) {
                    
        }
    }
}

There are two distinct steps to make now. The first is to declare a state property that stores the scroll position of the scroll view. The data type of this property is ScrollPosition, and we have to provide an initial value. The initializer should be any of the following:

  • The type of the elements presented in the scroll view, i.e., .init(idType: Int.self) here.

  • The edge (top, bottom, leading, trailing) that the scroll view should automatically scroll towards to on appear, i.e., .init(edge: .top).

  • An offset to the horizontal or vertical axis, i.e., .init(x: 100).

Unless there's a specific reason, the second way is the easiest to initialize a ScrollPosition property:

@State private var position: ScrollPosition = .init(edge: .top)

With that in place, the second step is to apply the scrollPosition(_:) modifier to the scroll view:

ScrollView {
    ...
}
.scrollPosition($position)
// ... other modifiers ...

See that we provide the binding value of the position state property as argument to scrollPosition(_:). The scroll view updates it when scrolling happens programmatically, but the same property gets updated when scrolling is happening manually too.

Scrolling to a specific item

In order to make it possible to scroll to a specific items or rows, it's necessary to uniquely identify them first. We manage that with the id(_:) modifier, providing a unique value for each item.

In this demonstration, we'll use the index of each random number in the array, as it's different for every element. We apply the modifier after the view that presents an item or a row content:

ForEach(randomNumbers.indices, id: \.self) { index in
    VStack {
        ...
    }
    .id(index)
    // ... other modifiers ...
}

We can now scroll to any item with a particular identifier using the scrollTo(id:) method, being accessible through the position property we declared previously. To make the example here more interesting, we'll add a button that:

  • Inserts a new random number to the randomNumbers array.

  • Scrolls to the index of that new item.

In the -still empty- toolbar and ToolbarItemGroup container, we will add a button as the following code shows:

.toolbar {
    ToolbarItemGroup(placement: .primaryAction) {
        Button("", systemImage: "plus.circle") {
            randomNumbers.append(Int.random(in: 0...1000))
            withAnimation {
                position.scrollTo(id: randomNumbers.count - 1)
            }
        }
    }
}

Notice the scrollTo(id:) method, and the fact that is included in an animation block. The animation here results to a smooth scrolling to the newly added item.

Similarly, we could scroll to any item with a specific id. For instance:

position.scrollTo(id: 15)

Scrolling to an edge

In the same fashion we can scroll programmatically to an edge; top, bottom, leading or trailing. There's a different method to use in this case, the scrollTo(edge:). It's important to note that we don't need to use the id(_:) modifier at all.

Let's add another button to the toolbar to demonstrate scrolling to the top edge:

Button("", systemImage: "arrowshape.up.circle") {
    position.scrollTo(edge: .top)
}

Scrolling to a specific point

Similarly as above, we can also scroll to a point either to the horizontal or the vertical axis, depending on the scrolling direction. The following example makes the scroll view scroll automatically in the vertical axis to a specific point:

Button("", systemImage: "numbers.rectangle") {
    position.scrollTo(y: 180)
}

Wrapping up

Scrolling programmatically with the scrollPosition(_:) modifier and the various ScrollPosition methods we met previously is a straightforward task. Unlike the ScrollViewReader which is a container view and everything has to be embedded to it, scrollPosition(_:) can be used view-wide, and that offers better flexibility. Just keep in mind that it's available in iOS 18 and above. Thanks for reading!