SwiftUI.cc
Snippets
LayoutScrollingIntermediate

SwiftUI ScrollViewReader and scrollTo

ScrollViewReader exposes a proxy whose scrollTo(_:anchor:) jumps to any child with an id — chat bottoms, section indexes, and back-to-top buttons.

4 min readUpdated 2026-06
Jump-to-bottom chat log
import SwiftUI

struct ChatLog: View {
    let messages: [String]

    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                LazyVStack(alignment: .leading, spacing: 8) {
                    ForEach(Array(messages.enumerated()), id: \.offset) { index, text in
                        Text(text)
                            .padding(10)
                            .background(.teal.opacity(0.15), in: .rect(cornerRadius: 12))
                            .id(index)
                    }
                }
                .padding()
            }
            .onChange(of: messages.count) {
                withAnimation {
                    proxy.scrollTo(messages.count - 1, anchor: .bottom)
                }
            }
        }
    }
}

ScrollViewReader preview

Proxy-driven jumps

ScrollViewReader wraps a scroll view and provides a ScrollViewProxy. Calling proxy.scrollTo(id, anchor:) scrolls until the child carrying that .id() reaches the anchor position. The proxy works inside List too, not just ScrollView.

Anchors

The UnitPoint anchor decides the alignment between target and viewport:

proxy.scrollTo("intro",   anchor: .top)     // header at top
proxy.scrollTo(lastID,     anchor: .bottom)  // tail visible
proxy.scrollTo(matchID,    anchor: .center)  // highlight centered

Omitting the anchor scrolls the minimal distance to make the target visible at all.

Reacting to data

The classic pattern pairs scrollTo with onChange so arrival of data triggers the jump. Be careful in chat UIs to skip auto-scrolling while the user is reading history — track whether they are near the bottom before forcing the jump.

ScrollViewReader vs scrollPosition

scrollPosition(id:) represents the visible item as state you can set, which covers most jump cases and stays testable. The proxy still earns its keep for anchor-precise jumps and for imperative flows like "scroll after this async write completes."

Common mistakes

  • Calling scrollTo before the target exists in the hierarchy — defer until data lands.
  • Relying on array offsets as ids while the array mutates, which retargets jumps.
  • Fighting lazy loading: a far-away lazy target may need an estimated jump first.

Related reference