Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Reviews, improves, and writes SwiftUI code following state management, view composition, performance, and iOS 26+ Liquid Glass best practices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/scroll-patterns.md
1# SwiftUI ScrollView Patterns Reference23## Table of Contents45- [ScrollViewReader for Programmatic Scrolling](#scrollviewreader-for-programmatic-scrolling)6- [Scroll Position Tracking](#scroll-position-tracking)7- [Scroll Transitions and Effects](#scroll-transitions-and-effects)8- [Scroll Target Behavior](#scroll-target-behavior)9- [Summary Checklist](#summary-checklist)1011## ScrollViewReader for Programmatic Scrolling1213**Use `ScrollViewReader` for scroll-to-top, scroll-to-bottom, and anchor-based jumps.**1415```swift16struct ChatView: View {17@State private var messages: [Message] = []18private let bottomID = "bottom"1920var body: some View {21ScrollViewReader { proxy in22ScrollView {23LazyVStack {24ForEach(messages) { message in25MessageRow(message: message)26.id(message.id)27}28Color.clear29.frame(height: 1)30.id(bottomID)31}32}33.onChange(of: messages.count) { _, _ in34withAnimation {35proxy.scrollTo(bottomID, anchor: .bottom)36}37}38.onAppear {39proxy.scrollTo(bottomID, anchor: .bottom)40}41}42}43}44```4546### Scroll-to-Top Pattern4748```swift49struct FeedView: View {50@State private var items: [Item] = []51@State private var scrollToTop = false52private let topID = "top"5354var body: some View {55ScrollViewReader { proxy in56ScrollView {57LazyVStack {58Color.clear59.frame(height: 1)60.id(topID)6162ForEach(items) { item in63ItemRow(item: item)64}65}66}67.onChange(of: scrollToTop) { _, shouldScroll in68if shouldScroll {69withAnimation {70proxy.scrollTo(topID, anchor: .top)71}72scrollToTop = false73}74}75}76}77}78```7980**Why**: `ScrollViewReader` provides programmatic scroll control with stable anchors. Always use stable IDs and explicit animations.8182## Scroll Position Tracking8384### Basic Scroll Position8586**Avoid** - Storing scroll position directly triggers view updates on every scroll frame:8788```swift89// ❌ Bad Practice - causes unnecessary re-renders90struct ContentView: View {91@State private var scrollPosition: CGFloat = 09293var body: some View {94ScrollView {95content96.background(97GeometryReader { geometry in98Color.clear99.preference(100key: ScrollOffsetPreferenceKey.self,101value: geometry.frame(in: .named("scroll")).minY102)103}104)105}106.coordinateSpace(name: "scroll")107.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in108scrollPosition = value109}110}111}112```113114**Preferred** - Check scroll position and update a flag based on thresholds for smoother, more efficient scrolling:115116```swift117// ✅ Good Practice - only updates state when crossing threshold118struct ContentView: View {119@State private var startAnimation: Bool = false120121var body: some View {122ScrollView {123content124.background(125GeometryReader { geometry in126Color.clear127.preference(128key: ScrollOffsetPreferenceKey.self,129value: geometry.frame(in: .named("scroll")).minY130)131}132)133}134.coordinateSpace(name: "scroll")135.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in136if value < -100 {137startAnimation = true138} else {139startAnimation = false140}141}142}143}144145struct ScrollOffsetPreferenceKey: PreferenceKey {146static var defaultValue: CGFloat = 0147static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {148value = nextValue()149}150}151```152153### Scroll-Based Header Visibility154155```swift156struct ContentView: View {157@State private var showHeader = true158159var body: some View {160VStack(spacing: 0) {161if showHeader {162HeaderView()163.transition(.move(edge: .top))164}165166ScrollView {167content168.background(169GeometryReader { geometry in170Color.clear171.preference(172key: ScrollOffsetPreferenceKey.self,173value: geometry.frame(in: .named("scroll")).minY174)175}176)177}178.coordinateSpace(name: "scroll")179.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in180if offset < -50 { // Scrolling down181withAnimation { showHeader = false }182} else if offset > 50 { // Scrolling up183withAnimation { showHeader = true }184}185}186}187}188}189```190191## Scroll Transitions and Effects192193> **iOS 17+**: All APIs in this section require iOS 17 or later.194195### Scroll-Based Opacity196197```swift198struct ParallaxView: View {199var body: some View {200ScrollView {201LazyVStack(spacing: 20) {202ForEach(items) { item in203ItemCard(item: item)204.visualEffect { content, geometry in205let frame = geometry.frame(in: .scrollView)206let distance = min(0, frame.minY)207return content208.opacity(1 + distance / 200)209}210}211}212}213}214}215```216217### Parallax Effect218219```swift220struct ParallaxHeader: View {221var body: some View {222ScrollView {223VStack(spacing: 0) {224Image("hero")225.resizable()226.aspectRatio(contentMode: .fill)227.frame(height: 300)228.visualEffect { content, geometry in229let offset = geometry.frame(in: .scrollView).minY230return content231.offset(y: offset > 0 ? -offset * 0.5 : 0)232}233.clipped()234235ContentView()236}237}238}239}240```241242## Scroll Target Behavior243244> **iOS 17+**: All APIs in this section require iOS 17 or later.245246### Paging ScrollView247248```swift249struct PagingView: View {250var body: some View {251ScrollView(.horizontal) {252LazyHStack(spacing: 0) {253ForEach(pages) { page in254PageView(page: page)255.containerRelativeFrame(.horizontal)256}257}258.scrollTargetLayout()259}260.scrollTargetBehavior(.paging)261}262}263```264265### Snap to Items266267```swift268struct SnapScrollView: View {269var body: some View {270ScrollView(.horizontal) {271LazyHStack(spacing: 16) {272ForEach(items) { item in273ItemCard(item: item)274.frame(width: 280)275}276}277.scrollTargetLayout()278}279.scrollTargetBehavior(.viewAligned)280.contentMargins(.horizontal, 20)281}282}283```284285## Summary Checklist286287- [ ] Use `ScrollViewReader` with stable IDs for programmatic scrolling288- [ ] Always use explicit animations with `scrollTo()`289- [ ] Use `.visualEffect` for scroll-based visual changes290- [ ] Use `.scrollTargetBehavior(.paging)` for paging behavior291- [ ] Use `.scrollTargetBehavior(.viewAligned)` for snap-to-item behavior292- [ ] Gate frequent scroll position updates by thresholds293- [ ] Use preference keys for custom scroll position tracking294