ScrollViewReader lets you drive a scroll view (proxy.scrollTo), but it never tells you where the scroll view currently is. Anything reactive — a "you are here" indicator, syncing two lists — needed a separate GeometryReader/PreferenceKey rig bolted on top.
#The old way
struct OldPhotoScroller: View {
let photos: [Photo] = (1...20).map { Photo(id: $0, title: "Photo \($0)") }
var body: some View {
ScrollViewReader { proxy in
VStack {
ScrollView {
LazyVStack {
ForEach(photos) { photo in
Text(photo.title)
.id(photo.id)
}
}
}
Button("Jump to Photo 15") {
withAnimation {
proxy.scrollTo(15, anchor: .center)
}
}
}
}
}
}
proxy can push a position but can't report one back.
#The new way
import SwiftUI
struct Photo: Identifiable {
let id: Int
let title: String
}
struct PhotoGridScroller: View {
let photos: [Photo] = (1...20).map { Photo(id: $0, title: "Photo \($0)") }
@State private var scrolledID: Int?
var body: some View {
VStack {
ScrollView {
LazyVStack {
ForEach(photos) { photo in
Text(photo.title)
.frame(maxWidth: .infinity)
.padding()
.id(photo.id)
}
}
.scrollTargetLayout()
}
.scrollPosition(id: $scrolledID)
Button("Jump to Photo 15") {
withAnimation {
scrolledID = 15
}
}
if let scrolledID {
Text("Currently near photo \(scrolledID)")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
#Why it matters
- One
Binding<ID?>replaces both the write path (scrollTo) and a read path you'd otherwise have to build yourself. - Setting
scrolledIDprogrammatically animates the scroll; reading it back tells you what the user scrolled to. - Works with any
Identifiableid type, not just integers — plug in UUIDs from real models directly. - Pairs naturally with
.scrollTargetBehavior(.viewAligned)for "which card is centered" style UI.
#Gotcha
The binding only tracks scroll position if .scrollTargetLayout() is applied to the LazyVStack/LazyHStack being scrolled. Without it, writing to scrolledID still visually scrolls the view — the write path works — but the binding never updates as the user scrolls by hand, so it silently stays stuck at whatever you last set it to (or nil). The asymmetry is easy to miss in testing if you only ever tap the "jump to" button and never scroll manually.