Detect Scroll Position in SwiftUI

Saeid Rezaeisadrabadi
3 min readFeb 27, 2023

--

In this story, I’m going to talk about how to detect scroll positions and show some information based on that.

One powerful feature of SwiftUI is its ability to handle scroll views and scroll position with ease. In this story, we’ll explore how to use the coordinateSpace modifier in SwiftUI to detect scroll position.

What is coordinateSpace?

coordinateSpace is a modifier in SwiftUI that lets you define a custom coordinate space for a view. A coordinate space is a system of coordinates that you can use to specify the position and size of views. By default, all views in SwiftUI use the same coordinate space, which is based on the top-left corner of the screen.

Using the coordinateSpace modifier, you can create a new coordinate space that's based on a specific view. This is useful when you want to track the position of a view relative to another view, rather than the position of the view relative to the screen.

Detecting scroll position with coordinateSpace

To detect the scroll position of a scroll view in SwiftUI, we are using the GeometryReader to get the position and size of the scroll view, while GeometryReader is mostly used to access size of the view, it also can be asked to read frame of the current view relative to a given coordinate system. then we use the coordinateSpace modifier to get the position of a child view relative to the scroll view.

Let’s jump to the code:

struct DetectScrollPosition: View {
@State private var scrollPosition: CGPoint = .zero

var body: some View {
ScrollView {
VStack {
ForEach((1...50), id: \.self) { row in
Text("Row \(row)")
.frame(height: 30)
.id(row)
}
}
}
.coordinateSpace(name: "scroll")
}
}

To demonstrate a View, I created a ScrollView with some sample data and set the coordinateSpacename of the ScrollView.

Then we are using GeometryReader to get the position and size of the scroll view, and background modifier to get the position of the scroll.

.background(GeometryReader { geometry in
Color.clear
.preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scroll")).origin)
})
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
self.scrollOffset = value
}

I also defined a custom preference key called ScrollOffsetPreferenceKey, which stores the scroll offset value as a preference.

struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGPoint = .zero

static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {
}
}

The reason we use SwiftUI’s preference system above is that our GeometryReader will be invoked as part of the view updating process, and we’re not allowed to directly mutate our view’s state during that process. So, by using a preference instead, we can deliver our CGPoint values to our view in an asynchronous fashion, which then lets us assign those values to our position binding.

Now We have the scroll position and we can use it to show some animation or show/hide UI field.

The complete implementation with a navigation title to show the current scroll position:

import SwiftUI

struct DetectScrollPosition: View {
@State private var scrollPosition: CGPoint = .zero

var body: some View {
NavigationView {
ScrollView {
VStack {
ForEach((1...50), id: \.self) { row in
Text("Row \(row)")
.frame(height: 30)
.id(row)
}
}
.background(GeometryReader { geometry in
Color.clear
.preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scroll")).origin)
})
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
self.scrollPosition = value
}
}
.coordinateSpace(name: "scroll")
.navigationTitle("Scroll offset: \(scrollPosition.y)")
.navigationBarTitleDisplayMode(.inline)
}
}
}

struct DetectScrollPosition_Previews: PreviewProvider {
static var previews: some View {
DetectScrollPosition()
}
}

struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGPoint = .zero

static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {
}
}

That’s it, we’ve seen how to use the coordinateSpace modifier in SwiftUI to detect scroll position. By defining a custom coordinate space for a view, we can track the position of the scroll. I hope that you found this story useful.

--

--