TechnologiesSwiftUI

ScrollViewReader struct

iOSmacOStvOSwatchOSvisionOSiOS 14.0+✓ renders

A view that provides programmatic scrolling, by working with a proxy

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.

  1. Wrap the scroll view and capture the proxy

    ScrollViewReader takes a view-builder closure whose single parameter is a ScrollViewProxy. You construct it as ScrollViewReader { proxy in ... }, and the proxy value 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.

  2. 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 integers 0..<100 as addressable anchors the proxy can later resolve.

  3. Drive scrolling with proxy.scrollTo(_:anchor:)

    ScrollViewProxy.scrollTo(_:anchor:) moves the enclosing scroll view so the view with the matching identifier becomes visible. The Button calls proxy.scrollTo(50, anchor: .top), telling the scroll view to bring the row whose id is 50 into view. Because the call happens in an action closure, the scroll is triggered by code, not by dragging.

  4. Position the target with the anchor parameter

    The optional anchor argument is a UnitPoint that controls where the target lands within the visible area — .top aligns it to the top edge, while .center or .bottom place it elsewhere; passing nil scrolls only the minimum distance needed. The example uses anchor: .top so row 50 settles against the top of the ScrollView.

  5. Keep the scroll view inside the reader

    The proxy only governs scroll views that are descendants of the ScrollViewReader, so the ScrollView (and its LazyVStack of identified rows) must live inside the reader's closure. The Button can sit alongside the ScrollView in the same VStack and still command it, because both share the same proxy.

Try it — Change 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.

ScrollViewReader.swift
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()
                }
            }
        }
    }
}
Live preview
Jump to 50 Row 0 Row 1 Row 2 Row 3 Row 4 Row 5 Row 6 Row 7 Row 8 Row 9 Row 10 Row 11 Row 12 Row 13 Row 14 Row 15 Row 16 Row 17 Row 18 Row 19 Row 20 Row 21 Row 22 Row 23 Row 24 Row 25 Row 26 Row 27 Row 28 Row 29 Row 30 Row 31 Row 32 Row 33 Row 34 Row 35 Row 36 Row 37 Row 38 Row 39 Row 40 Row 41 Row 42 Row 43 Row 44 Row 45 Row 46 Row 47 Row 48 Row 49 Row 50 Row 51 Row 52 Row 53 Row 54 Row 55 Row 56 Row 57 Row 58 Row 59 Row 60 Row 61 Row 62 Row 63 Row 64 Row 65 Row 66 Row 67 Row 68 Row 69 Row 70 Row 71 Row 72 Row 73 Row 74 Row 75 Row 76 Row 77 Row 78 Row 79 Row 80 Row 81 Row 82 Row 83 Row 84 Row 85 Row 86 Row 87 Row 88 Row 89 Row 90 Row 91 Row 92 Row 93 Row 94 Row 95 Row 96 Row 97 Row 98 Row 99
Jump to 50 Row 0 Row 1 Row 2 Row 3 Row 4 Row 5 Row 6 Row 7 Row 8 Row 9 Row 10 Row 11 Row 12 Row 13 Row 14 Row 15 Row 16 Row 17 Row 18 Row 19 Row 20 Row 21 Row 22 Row 23 Row 24 Row 25 Row 26 Row 27 Row 28 Row 29 Row 30 Row 31 Row 32 Row 33 Row 34 Row 35 Row 36 Row 37 Row 38 Row 39 Row 40 Row 41 Row 42 Row 43 Row 44 Row 45 Row 46 Row 47 Row 48 Row 49 Row 50 Row 51 Row 52 Row 53 Row 54 Row 55 Row 56 Row 57 Row 58 Row 59 Row 60 Row 61 Row 62 Row 63 Row 64 Row 65 Row 66 Row 67 Row 68 Row 69 Row 70 Row 71 Row 72 Row 73 Row 74 Row 75 Row 76 Row 77 Row 78 Row 79 Row 80 Row 81 Row 82 Row 83 Row 84 Row 85 Row 86 Row 87 Row 88 Row 89 Row 90 Row 91 Row 92 Row 93 Row 94 Row 95 Row 96 Row 97 Row 98 Row 99
swift → lexer → parser → sema → uiir → canvas Open in Studio ↗
What's new in SwiftUI 27 →