Skip to content

ScrollView Scroll Geometry (SwiftUI)

A SwiftUI guide for coding agents. Also covers onscrollgeometrychange, read scroll offset gesture, scrollgeometry contentoffset, track visible rect scroll, scroll offset observer ios 18.

The onScrollGeometryChange view modifier reads the live geometry of a ScrollView while the user scrolls, which ScrollPosition cannot do: its readable properties go nil the moment a gesture takes over. ScrollGeometry carries the content offset, bounds, container size, visible rect, content insets, and content size.

Tracking the Content Offset

The modifier takes three parts: the type you reduce the geometry to, a transformation closure that extracts it, and an action closure receiving the old and new values.

@State private var offsetY: CGFloat = 0

ScrollView {
  ForEach(1..<100, id: \.self) { number in
    Text(verbatim: number.formatted())
      .id(number)
  }
}
.onScrollGeometryChange(for: CGFloat.self) { geometry in
  geometry.contentOffset.y
} action: { oldValue, newValue in
  if oldValue != newValue {
    offsetY = newValue
  }
}

The explicit type exists for performance: geometry changes on every frame of a scroll, so SwiftUI re-runs the action only when the transformed value changes, not on every raw geometry change. Extract the smallest value that answers your question.

Combining Properties

To watch more than one property, return one equatable struct from the transformation.

struct ScrollData: Equatable {
  let size: CGSize
  let visible: CGRect
}

@State private var scrollData = ScrollData(size: .zero, visible: .zero)

ScrollView { /* content */ }
  .onScrollGeometryChange(for: ScrollData.self) { geometry in
    ScrollData(size: geometry.contentSize, visible: geometry.visibleRect)
  } action: { oldValue, newValue in
    if oldValue != newValue {
      scrollData = newValue
    }
  }

Division of Labor

Use ScrollPosition to drive the scroll view from code, and onScrollGeometryChange to observe what the user does with it. The two bind to the same scroll view without conflict.

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-scroll-geometry" })
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