How it works
ScrollViewReader is a view that gives you programmatic control over the scroll position of a scroll view it contains. Ordinarily a ScrollView scrolls only in response to user gestures; when you need to move it from code — to jump to a freshly inserted item, return to the top, or reveal a search result — you wrap the scroll view in a ScrollViewReader and use the proxy it hands you. Reach for it whenever a button, a state change, or an event must drive scrolling rather than the user's finger.
Wrap the scroll view and capture the proxy
ScrollViewReadertakes a view-builder closure whose single parameter is aScrollViewProxy. You construct it asScrollViewReader { proxy in ... }, and theproxyvalue stays in scope for the entire subtree, so any control inside can request a scroll. The reader itself draws nothing; it just establishes the coordinate context around its content.Tag each scroll destination with id(_:)
The proxy can only scroll to targets it can identify, so every view you might jump to must carry a stable identity via the
id(_:)modifier. Here each row is tagged with.id(i), registering the integers0..<100as addressable anchors the proxy can later resolve.Drive scrolling with proxy.scrollTo(_:anchor:)
ScrollViewProxy.scrollTo(_:anchor:)moves the enclosing scroll view so the view with the matching identifier becomes visible. TheButtoncallsproxy.scrollTo(50, anchor: .top), telling the scroll view to bring the row whoseidis50into view. Because the call happens in an action closure, the scroll is triggered by code, not by dragging.Position the target with the anchor parameter
The optional
anchorargument is aUnitPointthat controls where the target lands within the visible area —.topaligns it to the top edge, while.centeror.bottomplace it elsewhere; passingnilscrolls only the minimum distance needed. The example usesanchor: .topso row50settles against the top of theScrollView.Keep the scroll view inside the reader
The proxy only governs scroll views that are descendants of the
ScrollViewReader, so theScrollView(and itsLazyVStackof identified rows) must live inside the reader's closure. TheButtoncan sit alongside theScrollViewin the sameVStackand still command it, because both share the sameproxy.
proxy.scrollTo(50, anchor: .top) to proxy.scrollTo(99, anchor: .bottom) and watch the reader snap the last row to the bottom edge instead.Example & preview
Press Run live & edit to compile it in your browser — then edit the Swift on the left and the preview re-renders live.
struct ScrollViewReaderDemo: View {
var body: some View {
ScrollViewReader { proxy in
VStack {
Button("Jump to 50") {
proxy.scrollTo(50, anchor: .top)
}
.padding()
ScrollView {
LazyVStack(spacing: 12) {
ForEach(0..<100) { i in
Text("Row \(i)")
.frame(maxWidth: .infinity)
.padding(8)
.background(Color.blue.opacity(0.1))
.id(i)
}
}
.padding()
}
}
}
}
}