Last active
February 19, 2026 07:03
-
-
Save fxm90/5bc949e4d6f2f56901b47250a25fc64d to your computer and use it in GitHub Desktop.
A SwiftUI example demonstrating how to track the horizontal offset in a scroll view.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // ContentView.swift | |
| // | |
| // Created by Felix Mau on 07.02.26. | |
| // Copyright © 2026 Felix Mau. All rights reserved. | |
| // | |
| import SwiftUI | |
| private enum Config { | |
| /// The name of the coordinate space used to measure the scroll view’s content | |
| /// relative to the scroll view itself. | |
| static let scrollViewCoordinateSpace = "scrollViewCoordinateSpace" | |
| } | |
| /// A SwiftUI view that demonstrates how to observe the horizontal content offset | |
| /// of a `ScrollView` using a named coordinate space and geometry updates. | |
| /// | |
| /// To track the horizontal offset, this implementation uses `onGeometryChange(for:of:action:)` | |
| /// to observe changes in the scroll view’s geometry and derive the current horizontal offset. | |
| /// This API is available starting in iOS 16. | |
| /// | |
| /// # Apps Targeting iOS 18.0+ | |
| /// | |
| /// If your app targets iOS 18 or later, prefer `onScrollGeometryChange(for:of:action:)`, | |
| /// which provides a more direct (and scroll-specific way) to observe geometry changes | |
| /// related to scrolling. | |
| struct ContentView: View { | |
| /// The current horizontal scroll offset of the scroll view’s content. | |
| /// | |
| /// This value updates as the user scrolls. A negative value indicates | |
| /// scrolling to the right, while zero means the content is aligned | |
| /// with the leading edge. | |
| @State | |
| private var horizontalScrollOffset: CGFloat = 0 { | |
| didSet { | |
| // Useful for debugging or driving side effects such as animations. | |
| print("Horizontal scroll offset", horizontalScrollOffset) | |
| } | |
| } | |
| var body: some View { | |
| ScrollView(.horizontal) { | |
| LazyHStack { | |
| ForEach(0 ..< 3) { index in | |
| VStack(alignment: .leading) { | |
| Text("Hello World \(index + 1)") | |
| .font(.headline) | |
| Text("Lorem ipsum dolor sit amet.") | |
| .font(.body) | |
| } | |
| .frame(width: 375, alignment: .leading) | |
| } | |
| } | |
| // Observe geometry changes for the scroll content. | |
| // | |
| // This modifier extracts the `minX` value of the content's frame | |
| // within the named coordinate space, which effectively represents | |
| // the horizontal scroll offset. | |
| .onGeometryChange(for: CGFloat.self) { geometry in | |
| geometry.frame(in: .named(Config.scrollViewCoordinateSpace)).minX | |
| } action: { horizontalScrollOffset in | |
| self.horizontalScrollOffset = horizontalScrollOffset | |
| } | |
| } | |
| .scrollIndicators(.hidden) | |
| // Define a named coordinate space so we can measure the scroll content | |
| // relative to the scroll view itself. | |
| .coordinateSpace(name: Config.scrollViewCoordinateSpace) | |
| .frame(width: 375, height: 80) | |
| } | |
| } | |
| // MARK: - Preview | |
| #Preview { | |
| ContentView() | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // ContentViewWithSnapping.swift | |
| // | |
| // Created by Felix Mau on 07.02.26. | |
| // Copyright © 2026 Felix Mau. All rights reserved. | |
| // | |
| import SwiftUI | |
| private enum Config { | |
| /// The name of the coordinate space used to measure the scroll view’s content | |
| /// relative to the scroll view itself. | |
| static let scrollViewCoordinateSpace = "scrollViewCoordinateSpace" | |
| } | |
| /// A view that demonstrates how to observe the horizontal scroll offset | |
| /// of a `ScrollView` in SwiftUI using a named coordinate space and geometry updates. | |
| /// | |
| /// This example further demonstrates how to implement a **snapping behavior** by setting | |
| /// the scroll target behavior to `.viewAligned`. | |
| /// | |
| /// To track the horizontal offset, this implementation uses `onGeometryChange(for:of:action:)` | |
| /// to observe changes in the scroll view’s geometry and derive the current horizontal offset. | |
| /// This API is available starting in iOS 16. | |
| /// | |
| /// # Apps Targeting iOS 18.0+ | |
| /// | |
| /// If your app targets iOS 18 or later, prefer `onScrollGeometryChange(for:of:action:)`, | |
| /// which provides a more direct (and scroll-specific way) to observe geometry changes | |
| /// related to scrolling. | |
| struct ContentViewWithSnapping: View { | |
| /// The current horizontal scroll offset of the scroll view’s content. | |
| /// | |
| /// This value updates as the user scrolls. A negative value indicates | |
| /// scrolling to the right, while zero means the content is aligned | |
| /// with the leading edge. | |
| @State | |
| private var horizontalScrollOffset: CGFloat = 0 { | |
| didSet { | |
| // Useful for debugging or driving side effects such as animations. | |
| print("Horizontal scroll offset", horizontalScrollOffset) | |
| } | |
| } | |
| var body: some View { | |
| ScrollView(.horizontal) { | |
| LazyHStack { | |
| ForEach(0 ..< 3) { index in | |
| VStack(alignment: .leading) { | |
| Text("Hello World \(index + 1)") | |
| .font(.headline) | |
| Text("Lorem ipsum dolor sit amet.") | |
| .font(.body) | |
| } | |
| .frame(width: 375, alignment: .leading) | |
| } | |
| } | |
| // Marks the stack’s children as scroll targets. | |
| // | |
| // This tells SwiftUI that each child view inside the layout | |
| // can be snapped to when used in combination with the view modifier | |
| // `scrollTargetBehavior`. | |
| .scrollTargetLayout() | |
| // Observe geometry changes for the scroll content. | |
| // | |
| // This modifier extracts the `minX` value of the content's frame | |
| // within the named coordinate space, which effectively represents | |
| // the horizontal scroll offset. | |
| .onGeometryChange(for: CGFloat.self) { geometry in | |
| geometry.frame(in: .named(Config.scrollViewCoordinateSpace)).minX | |
| } action: { horizontalScrollOffset in | |
| self.horizontalScrollOffset = horizontalScrollOffset | |
| } | |
| } | |
| .scrollIndicators(.hidden) | |
| // Enables snapping behavior when scrolling ends. | |
| // | |
| // The `.viewAligned` behavior causes the scroll view to settle | |
| // on the nearest child view that was marked as a scroll target | |
| // via `.scrollTargetLayout()`. | |
| // | |
| // In this example, each fixed-width child view snaps neatly | |
| // into alignment as the user lifts their finger. | |
| .scrollTargetBehavior(.viewAligned) | |
| // Define a named coordinate space so we can measure the scroll content | |
| // relative to the scroll view itself. | |
| .coordinateSpace(name: Config.scrollViewCoordinateSpace) | |
| .frame(width: 375, height: 80) | |
| } | |
| } | |
| // MARK: - Preview | |
| #Preview { | |
| ContentViewWithSnapping() | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example
scroll-view-example.mov