All skills
avdlee avatar

/swiftui-expert-skill

@1e522cf

Use when writing, reviewing, or refactoring SwiftUI code for iOS or macOS, including state and `@Observable` data flow, view composition, resizable layouts, safe areas, display scale, performance, lists, environment, localization, animation, Liquid Glass, and API migration. Also use for iPhone Duo, foldable, or large-display layouts (`NavigationSplitView` on large displays, two-column reflow, foldable grids, `ArrangementView`, `ReservedRegion`), hinge effects, vertical bars, `@State` initialization or synthesized-property diagnostics, `@ContentBuilder` ambiguity, `reorderable` drag/drop, custom `AsyncImage` `URLSession`, swipe actions outside List, item-bound `alert`/`confirmationDialog`, `ToolbarOverflowMenu`, `AnimatableValues`, Document APIs (`Document`/`DocumentReader`), and Instruments `.trace` capture or analysis.

Use this Skill: https://skilld.dev/gh/avdlee/swiftui-agent-skill/swiftui-expert-skill

This session only. Nothing lands on disk.

referencesscroll-patterns.md

≈2.7k tokens on demand. Your agent reads this file only when SKILL.md points to it.

SwiftUI ScrollView Patterns Reference

Table of Contents

Choose the Appropriate Scroll API

  • On iOS 18+, use onScrollGeometryChange(for:of:action:) to observe scroll geometry.
  • On iOS 18+, use scrollPosition(_:) with ScrollPosition to scroll by identity, offset, or edge.
  • On iOS 17+, use scrollPosition(id:) when an optional ID binding is sufficient.
  • Use ScrollViewReader when proxy-based scrolling or support for earlier versions is needed.

ScrollViewReader for Programmatic Scrolling

Use ScrollViewReader for proxy-based scroll-to-top, scroll-to-bottom, and anchor-based jumps.

struct ChatView: View {
    @State private var messages: [Message] = []
    private let bottomID = "bottom"
    
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                LazyVStack {
                    ForEach(messages) { message in
                        MessageRow(message: message)
                            .id(message.id)
                    }
                    Color.clear
                        .frame(height: 1)
                        .id(bottomID)
                }
            }
            .onChange(of: messages.count) { _, _ in
                withAnimation {
                    proxy.scrollTo(bottomID, anchor: .bottom)
                }
            }
            .onAppear {
                proxy.scrollTo(bottomID, anchor: .bottom)
            }
        }
    }
}

Scroll-to-Top Pattern

struct FeedView: View {
    @State private var items: [Item] = []
    @State private var scrollToTop = false
    private let topID = "top"
    
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView {
                LazyVStack {
                    Color.clear
                        .frame(height: 1)
                        .id(topID)
                    
                    ForEach(items) { item in
                        ItemRow(item: item)
                    }
                }
            }
            .onChange(of: scrollToTop) { _, shouldScroll in
                if shouldScroll {
                    withAnimation {
                        proxy.scrollTo(topID, anchor: .top)
                    }
                    scrollToTop = false
                }
            }
        }
    }
}

Why: ScrollViewReader provides proxy-based programmatic scroll control. Use stable IDs for scroll targets, and add animation when an animated transition is appropriate.

Scroll Position Tracking

iOS 18+: Use onScrollGeometryChange(for:of:action:) to observe scroll geometry and scrollPosition(_:) with a ScrollPosition binding for flexible programmatic scrolling. For iOS 17, use scrollPosition(id:) with an optional ID binding.

Observe Scroll Geometry (iOS 18+)

onScrollGeometryChange transforms frequently changing ScrollGeometry into an Equatable value and runs its action when that transformed value changes. Extract the smallest value needed by the feature.

When exact offset tracking is required, extract contentOffset. This value normally changes on every scrolling frame, so avoid using it to update large or expensive view hierarchies:

struct OffsetTrackingView: View {
    @State private var scrollOffset: CGFloat = 0

    var body: some View {
        ScrollView {
            content
        }
        .onScrollGeometryChange(for: CGFloat.self) { geometry in
            geometry.contentOffset.y
        } action: { _, newValue in
            scrollOffset = newValue
        }
    }
}

When only a threshold matters, transform the geometry into a Bool so the action runs only when the scroll view crosses that threshold. The header visibility example below demonstrates this pattern.

Programmatic Scroll Position (iOS 18+)

The unlabeled scrollPosition(_:) overload requires a Binding<ScrollPosition>. Add scrollTargetLayout() to the layout containing the identified views:

struct ProgrammaticScrollView: View {
    @State private var position = ScrollPosition(idType: Item.ID.self)

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(items) { item in
                    ItemRow(item: item)
                }
            }
            .scrollTargetLayout()
        }
        .scrollPosition($position)
        .toolbar {
            Button("Scroll to First") {
                if let firstID = items.first?.id {
                    withAnimation {
                        position.scrollTo(id: firstID)
                    }
                }
            }
        }
    }
}

For iOS 17, bind the ID using the labeled overload instead:

@State private var scrolledID: Item.ID?

ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemRow(item: item)
        }
    }
    .scrollTargetLayout()
}
.scrollPosition(id: $scrolledID)

Scroll-Based Header Visibility

Extracting the threshold as a Bool avoids running the action for every offset change:

struct ContentView: View {
    @State private var showHeader = true

    var body: some View {
        VStack(spacing: 0) {
            if showHeader {
                HeaderView()
                    .transition(.move(edge: .top))
            }

            ScrollView {
                content
            }
            .onScrollGeometryChange(for: Bool.self) { geometry in
                geometry.contentOffset.y + geometry.contentInsets.top > 50
            } action: { _, isPastThreshold in
                withAnimation {
                    showHeader = !isPastThreshold
                }
            }
        }
    }
}
<details> <summary>Pre-iOS 18 compatibility — GeometryReader + PreferenceKey</summary>

Use this approach when supporting iOS 17 or earlier. GeometryReader and preferences remain available, but require a named coordinate space and a custom PreferenceKey.

struct ContentView: View {
    @State private var showHeader = true

    var body: some View {
        VStack(spacing: 0) {
            if showHeader {
                HeaderView()
                    .transition(.move(edge: .top))
            }

            ScrollView {
                content
                    .background(
                        GeometryReader { geometry in
                            Color.clear
                                .preference(
                                    key: ScrollOffsetPreferenceKey.self,
                                    value: geometry.frame(in: .named("scroll")).minY
                                )
                        }
                    )
            }
            .coordinateSpace(.named("scroll"))
            .onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
                let shouldShowHeader = offset >= -50
                if shouldShowHeader != showHeader {
                    withAnimation {
                        showHeader = shouldShowHeader
                    }
                }
            }
        }
    }
}

struct ScrollOffsetPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}
</details>

Scroll Transitions and Effects

iOS 17+: All APIs in this section require iOS 17 or later.

Scroll-Based Opacity

struct ParallaxView: View {
    var body: some View {
        ScrollView {
            LazyVStack(spacing: 20) {
                ForEach(items) { item in
                    ItemCard(item: item)
                        .visualEffect { content, geometry in
                            let frame = geometry.frame(in: .scrollView)
                            let distance = min(0, frame.minY)
                            return content
                                .opacity(1 + distance / 200)
                        }
                }
            }
        }
    }
}

Parallax Effect

struct ParallaxHeader: View {
    var body: some View {
        ScrollView {
            VStack(spacing: 0) {
                Image("hero")
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(height: 300)
                    .visualEffect { content, geometry in
                        let offset = geometry.frame(in: .scrollView).minY
                        return content
                            .offset(y: offset > 0 ? -offset * 0.5 : 0)
                    }
                    .clipped()
                
                ContentView()
            }
        }
    }
}

Scroll Target Behavior

iOS 17+: All APIs in this section require iOS 17 or later.

Paging ScrollView

struct PagingView: View {
    var body: some View {
        ScrollView(.horizontal) {
            LazyHStack(spacing: 0) {
                ForEach(pages) { page in
                    PageView(page: page)
                        .containerRelativeFrame(.horizontal)
                }
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.paging)
    }
}

Snap to Items

struct SnapScrollView: View {
    var body: some View {
        ScrollView(.horizontal) {
            LazyHStack(spacing: 16) {
                ForEach(items) { item in
                    ItemCard(item: item)
                        .frame(width: 280)
                }
            }
            .scrollTargetLayout()
        }
        .scrollTargetBehavior(.viewAligned)
        .contentMargins(.horizontal, 20)
    }
}

Summary Checklist

  • Use ScrollViewReader with stable IDs when proxy-based scrolling is needed
  • Use .visualEffect for scroll-based visual changes
  • Use .scrollTargetBehavior(.paging) for paging behavior
  • Use .scrollTargetBehavior(.viewAligned) for snap-to-item behavior
  • Use onScrollGeometryChange (iOS 18+) and extract only the value needed
  • Use scrollPosition(_:) with ScrollPosition for flexible scrolling on iOS 18+
  • Use scrollPosition(id:) with an optional ID binding on iOS 17+
  • Add .scrollTargetLayout() when scrolling to identified views
  • Derive threshold values instead of propagating every offset change when possible
  • Use the GeometryReader + preference approach when supporting pre-iOS 18 versions

Source: SKILL.md on GitHub

No alerts2d5 checks · Risk SAFE
  • Gen Agent Trust Hub2d

    The skill is a professional-grade assistant for SwiftUI development and performance profiling. It includes Python scripts to interface with the Xcode xctrace CLI for recording and analyzing Instruments traces. All identified code and instructions are consistent with its stated purpose and follow security best practices.

  • Socket2d

    No alerts

  • Snyk2d

    Risk: LOW · No issues

  • Runlayer6mo

    19 files scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 1e522cf. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 3 days ago
  • Performance
  • swiftui
  • ios
  • macos
  • instruments
  • state-management
  • view-composition
  • accessibility
  • animations

README badge

README badge for avdlee/swiftui-agent-skill/swiftui-expert-skill

Provides guidance for writing, reviewing, and refactoring SwiftUI code for iOS and macOS, including state management, view composition, performance optimization, and Instruments trace recording and analysis. Covers deprecated API detection, animation patterns, accessibility, and Liquid Glass adoption.

Generated from the current SKILL.md.

Does this skill work with both iOS and macOS?
Yes. The skill covers SwiftUI for both iOS and macOS, with dedicated reference sections for macOS-specific patterns like scenes, window styling, and views (HSplitView, Table, PasteButton).
Can this skill help me record and analyze Instruments traces?
Yes. The skill includes workflows to record traces via `record_trace.py` (with template selection for real devices vs simulators) and analyze them via `analyze_trace.py` to identify hangs, hitches, CPU hotspots, and excessive view updates.
Does this skill enforce a specific architecture pattern?
No. It focuses on correctness and performance without mandating MVVM, VIPER, or other architectural styles, though it encourages separating business logic from views for testability.
What does the skill do about deprecated APIs?
It consults `references/latest-apis.md` at the start of every task to identify and replace deprecated APIs with modern equivalents across iOS 15+ through iOS 26+, and gates version-specific APIs with `#available`.
Does this skill handle Liquid Glass effects?
Yes, but only when explicitly requested by the user. It includes guidance in `references/liquid-glass.md` for iOS 26+ Liquid Glass adoption with sensible fallbacks for earlier versions.

Generated from the current SKILL.md. These answers refresh after source changes.