Skip to content

Mastering ScrollView (SwiftUI)

A SwiftUI guide for coding agents. Also covers scrollview offset, scroll to position, scrollviewreader proxy, scroll position tracking ios, content offset preference key, programmatic scrolling.

ScrollView takes content in a ViewBuilder closure, with parameters for the axis and the indicator visibility. ScrollViewReader (iOS 14+) adds programmatic scrolling, and a PreferenceKey plus GeometryReader recovers the content offset the API does not expose.

Scroll to a Position

ScrollViewReader finds the first scroll view among its children and hands its closure a ScrollViewProxy. Call scrollTo(_:anchor:) with the id of any child to jump there.

ScrollViewReader { scrollView in
  ScrollView {
    Button("Scroll to bottom") {
      withAnimation {
        scrollView.scrollTo(99, anchor: .center)
      }
    }

    ForEach(0..<100) { index in
      Text(String(index))
        .id(index)
    }
  }
}

Each target view needs an explicit .id(...); the proxy matches on that value. The anchor aligns the found view within the viewport. scrollTo animates when wrapped in withAnimation. ScrollViewReader also works with List.

Read the Content Offset

The scroll view does not report its offset, so publish it yourself through a preference. A zero-sized GeometryReader at the top of the content reads its own origin in the scroll view's named coordinate space, which is the content offset.

private struct ScrollOffsetPreferenceKey: PreferenceKey {
  static var defaultValue: CGPoint = .zero
  static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {}
}

ScrollView(axes, showsIndicators: showsIndicators) {
  GeometryReader { geometry in
    Color.clear.preference(
      key: ScrollOffsetPreferenceKey.self,
      value: geometry.frame(in: .named("scrollView")).origin
    )
  }
  .frame(width: 0, height: 0)

  content
}
.coordinateSpace(name: "scrollView")
.onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: offsetChanged)

Wrap this in your own ScrollView<Content> type that mirrors the system initializer and adds an offsetChanged: (CGPoint) -> Void closure. Callers keep the familiar API and gain the offset stream for sticky headers, parallax, or scroll-linked effects.

Gotchas

  • The GeometryReader probe must sit inside the scroll content, before it, and be pinned to .frame(width: 0, height: 0) so it does not disturb the layout.
  • The frame must be read in the named coordinate space, not .global, or the value drifts with the scroll view's own position on screen.
  • On iOS 17+, prefer the native scrollPosition and onScrollGeometryChange APIs; this preference-key recipe is the back-deployable version.

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-mastering-scrollview" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems