SwiftUI · · 4 min read

Detecting Scroll Positions in ScrollView with SwiftUI

Detecting Scroll Positions in ScrollView with SwiftUI

One common question that arises when using scroll views in SwiftUI is how to detect the scroll position. Prior to the release of iOS 17, developers had to come up with their own solutions to capture the scroll positions. However, the new version of SwiftUI has updated ScrollView with a new modifier called scrollPosition. With this new feature, developers can effortlessly identify the item that is being scrolled to.

In this tutorial, we will take a closer look at this new modifier and examine how it can assist you in detecting scroll positions in scroll views.

Using the ScrollPosition Modifier to Detect Scroll Positions

Let’s begin with a simple scroll view that displays a list of 50 items.

struct ColorListView: View {
    let bgColors: [Color] = [ .yellow, .blue, .orange, .indigo, .green ]

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 10) {
                ForEach(0...50, id: \.self) { index in

                    bgColors[index % 5]
                        .frame(height: 100)
                        .overlay {
                            Text("\(index)")
                                .foregroundStyle(.white)
                                .font(.system(.title, weight: .bold))
                        }
                }
            }
        }
        .contentMargins(.horizontal, 10.0, for: .scrollContent)

    }
}

The code is quite straightforward if you are familiar with implementing a scroll view in SwiftUI. We utilize a ForEach loop to present 50 color items, which are then embedded within a vertical scroll view. If you add this code to a SwiftUI project, you should be able to preview it and see something similar to the figure below.

swiftui-scroll-view-demo

To keep track of the current scroll position or item, you should first declare a state variable to hold the position:

@State private var scrollID: Int?

Next, attach the scrollPosition modifier to the scroll view. This modifier takes a binding to scrollID, which stores the scroll position:

ScrollView {

  ...

}
.scrollPosition(id: $scrollID)

As the scroll view scrolls, the binding will be updated with the index of the color view.

Lastly, attach the scrollTargetLayout modifier to the LazyVStack view as follows:

LazyVStack(spacing: 10) {

    ...

}
.scrollTargetLayout()

Without the scrollTargetLayout() modifier, the scrollPosition modifier will not work correctly. The scrollPosition modifier relies on the scrollTargetLayout() modifier to configure which the layout that contains your scroll targets.

To observe the changes of the scroll position, you can attach a onChange modifier to the scroll view and print the scroll ID to the console:

.onChange(of: scrollID) { oldValue, newValue in
    print(newValue ?? "")
}

As you scroll through the list, the current scroll position should be displayed in the console.

swiftui-scroll-view-scroll-positions

Scroll to the Top

Let’s implement one more feature using the scrollPosition modifier. When a user taps any of the color views, the list should automatically scroll back to the top.

swiftui-scroll-view-scroll-to-top

This can be achieved by adding a onTapGesture modifier to each color view, and passing a closure that sets the scrollID to 0 within it. When the user taps on any of the color views, the scrollID will be updated to 0, which will cause the list to scroll back to the top.

bgColors[index % 5]
    .frame(height: 100)
    .overlay {
        Text("\(index)")
            .foregroundStyle(.white)
            .font(.system(.title, weight: .bold))
    }
    .onTapGesture {
        withAnimation {
            scrollID = 0
        }
    }

Adjusting Content Margins of Scroll Views

Now that you know how to detect the scroll position, let’s discuss one more new feature of scroll views in iOS 17. First, it’s the contentMargins modifier. This is a new feature in SwiftUI that allows developers to customize the margins of scrollable views. With contentMargins, you can easily adjust the amount of space between the content and the edges of the scroll view, giving you more control over the layout of your app.

We have already used this modifier in the sample code that sets the horizontal margin to 10 points.

.contentMargins(.horizontal, 10.0, for: .scrollContent)

The .scrollContent parameter value indicates that the content margin should only be applied to the scroll content, rather than the entire scroll view. If you do not specify the edges and placement parameters, the contentMargins modifier will apply the margins to all edges of the scroll view. For example, in the code below, it insets the entire scroll view by 50 points, including the scroll bar.

swiftui-scroll-view-contentmargins

In case if you want to keep the scroll bar at its original position, you can set the placement parameter to .scrollContent.

swiftui-scroll-view-scroll-content

Summary

The new scrollPosition modifier is one of the most anticipated features of scroll views. In iOS 17, SwiftUI finally introduces this feature allowing developers to detect scroll positions. In this tutorial, we have demonstrated the usage of this new modifier and introduced another new modifier called contentMargins, which enables developers to customize the margins of scrollable views.

With these new features in SwiftUI, developers can now effortlessly create more customized and visually appealing layouts for their apps.

If you’re interested in learning more about SwiftUI, don’t forget to check out our Mastering SwiftUI book.

Read next