Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

2026 DesignSwiftUI & UI Frameworks

WWDC26 · 21 min · Design / SwiftUI & UI Frameworks

Dive into lazy stacks and scrolling with SwiftUI

Discover the inner workings of lazy stacks in SwiftUI. We’ll explore how LazyVStack and LazyHStack estimate sizes, lazily load subviews, and prefetch content to deliver smooth scrolling experiences. We’ll also cover advanced performance optimizations, state management best practices, and tips for precise programmatic scrolling. To get the most out of this session, we recommend basic familiarity with SwiftUI layout using stacks.

Watch at developer.apple.com ↗

Transcript all transcripts

Chapters

Code shown on screen · 26 snippets

Origami app swift · at 1:23 ↗
// Origami app

struct ContentView: View {
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(steps) { step in
                    StepView(step: step)
                }
            }
        }
    }
}

struct StepView: View { /* ... */ }
Horizontally scrolling showcase swift · at 5:11 ↗
// Horizontally scrolling showcase

struct ContentView: View {
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(steps) { step in
                    StepView(step: step)
                }
                Showcase()
            }
        }
    }
}

struct StepView: View { /* ... */ }

struct Showcase: View {
    var body: some View {
        ScrollView(.horizontal) {
            LazyHStack {
                ForEach(photos) { photo in
                    PhotoView(photo: photo)
                }
            }
        }
    }
}
Showcase section swift · at 6:30 ↗
// Showcase section

struct ContentView: View {
    var body: some View {
        ScrollView {
            LazyVStack(pinnedViews: [.sectionHeaders]) {
                ForEach(steps) { step in
                    StepView(step: step)
                }
                Showcase()
            }
        }
    }
}

struct StepView: View { /* ... */ }
  
struct Showcase: View {
    var body: some View {
        Section {
            ForEach(photos) { photo in
                PhotoView(photo: photo)
            }
        } header: { /* ... */ }
    }
}
Scroll effect swift · at 7:04 ↗
// Scroll effect

struct ContentView: View { /* ... */ }

struct StepView: View { /* ... */ }

struct Showcase: View {
    var body: some View {
        Section {
            ForEach(photos) { photo in
                PhotoView(photo: photo)
                    .scrollTransition { effect, phase in
                        effect
                            .rotationEffect(.degrees(phase.value * 20))
                            .scaleEffect(1 + phase.value * 0.2)
                    }
            }
        } header: { /* ... */ }
    }
}
Scroll effect swift · at 7:36 ↗
// Scroll effect

struct ContentView: View { /* ... */ }

struct StepView: View { /* ... */ }

struct Showcase: View {
    var body: some View {
        Section {
            ForEach(photos) { photo in
                PhotoView(photo: photo)
                    .scrollTransition { effect, phase in
                        effect
                            .scaleEffect(1 - abs(phase.value) * 0.1)
                    }
            }
        } header: { /* ... */ }
    }
}
Scroll to Showcase button swift · at 8:20 ↗
// Absolute offset

struct ContentView: View {
    @State var isScrollToShowcaseVisible = false

    var body: some View {
        ScrollView { /* ... */ }
            .overlay(alignment: .bottom) { /* ... */ }
            .onScrollGeometryChange(for: Bool.self) { geo in
                geo.contentOffset.y <= 100
            } action: { _, newValue in
                self.isScrollToShowcaseVisible = newValue
            }
    }
}
Scroll to Showcase button swift · at 8:51 ↗
// Absolute offset

struct ContentView: View {
    @State var isScrollToShowcaseVisible = false

    var body: some View {
        ScrollView { /* ... */ }
            .overlay(alignment: .bottom) { /* ... */ }
            .onScrollTargetVisibilityChange(
                idType: Step.ID.self,
                threshold: 0.8
            ) { visibleIDs in
                isScrollToShowcaseVisible = shouldShowScrollButton(visibleIDs: visibleIDs)
            }
    }
}
One resolved subview swift · at 9:29 ↗
// Origami

struct ContentView: View {
    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(steps) { step in
                    StepView(step: step)
                }
            }
        }
    }
}

struct StepView: View { /* ... */ }
Multiple resolved subviews swift · at 10:03 ↗
// Multiple subviews

struct ContentView: View { /* ... */ }

struct StepView: View {
    let step: Step

    var body: some View {
        StepDiagram(/* ... */)
        StepInstructions(/* ... */)
    }
}
Dynamic number of subviews swift · at 10:52 ↗
// Dynamic number of views

struct ContentView: View { /* ... */ }

struct StepView: View {
    let step: Step

    @Environment(\.detailLevel) var detailLevel

    var body: some View {
        if step.isVisible(in: detailLevel) {
            VStack { /* ... */ }
        }
    }
}
Filtering on the view level swift · at 11:46 ↗
// Dynamic number of views

struct ContentView: View { /* ... */ }

struct StepView: View {
    let step: Step

    @Environment(\.detailLevel) var detailLevel
    @Environment(\.writingStyle) var writingStyle

    var body: some View {
        if step.isVisible(in: detailLevel) { /* ... */ }
    }
}
Filtering on the data level swift · at 12:15 ↗
// Filter at the data level

struct ContentView: View {
    @Query var steps: [Step]

    init(detailLevel: DetailLevel) {
        _steps = Query(filter: #Predicate<Step> { step in
            step.detailLevel >= detailLevel
        })
    }

    var body: some View { /* ... */ }
}

struct StepView: View { /* ... */ }
Optional unwrapping swift · at 12:35 ↗
// Optional unwrapping

struct ContentView: View { /* ... */ }

struct StepView: View {
    let step: Step

    @Environment(\.apiToken) var token

    var body: some View {
        if let token { /* ... */ }
    }
}
Optional unwrapping swift · at 12:48 ↗
// Optional unwrapping

struct ContentView: View { /* ... */ }

struct StepView: View {
    let step: Step

    @Environment(NetworkClient.self) var networkClient

    var body: some View { /* ... */ }
}
Loading more content swift · at 15:28 ↗
// Loading more content

struct Showcase: View {
    @State var pager = ShowcasePager()

    var body: some View {
        ForEach(pager.pages) { page in
            PageView(page: page)
        }
        if !pager.atEnd {
            ProgressView()
                .progressViewStyle(.circular)
                .onAppear {
                    pager.fetchPage()
                }
        }
    }
}
Setting up lazy stack subview in onAppear swift · at 15:53 ↗
// onAppear

struct StepView: View {
    let id: Step.ID
    @State var viewModel = StepViewModel()

    var body: some View {
        VStack {
            if let content = viewModel.content { /* ... */ }
        }
        .onAppear {
            viewModel.configure(with: id)
        }
    }
}
Lazy stack subview ready before onAppear swift · at 16:14 ↗
// onAppear

struct StepView: View {
    @State var viewModel: StepViewModel

    init(id: Step.ID) {
        _viewModel = State(initialValue: StepViewModel(id: id))
    }

    var body: some View { /* ... */ }
}
Loading diagram with task modifier swift · at 16:23 ↗
// Diagram loading

struct StepView: View {
    let step: Step
    @State var diagramLoader = DiagramLoader()

    @State var diagram: Diagram?

    var body: some View {
        VStack { /* ... */ }
            .task {
                diagram = await diagramLoader.loadDiagram(id: step.id)
            }
    }
}
Loading diagram in initializer swift · at 16:40 ↗
// Diagram loading

struct StepView: View {
    let step: Step
    @State var diagramLoader: DiagramLoader

    init(step: Step) {
        self.step = step
        _diagramLoader = State(initialValue: DiagramLoader(id: step.id))
    }

    var body: some View { /* ... */ }
}

@Observable
class DiagramLoader { /* ... */ }
Highlight @State variable swift · at 17:16 ↗
// Highlighting

struct ContentView: View { /* ... */ }

struct StepView: View {
    let step: Step
    @State var isHighlighted = false

    var body: some View { /* ... */ }
}
Highlight @Binding swift · at 17:33 ↗
// Highlighting

struct ContentView: View {
    @State var highlighted: Set<Step.ID> = []

    var body: some View { /* ... */ }
}

struct StepView: View {
    let step: Step
    @Binding var highlighted: Set<Step.ID>

    var body: some View { /* ... */ }
}
Programmatically scroll to showcase swift · at 17:58 ↗
// Programmatically scroll to showcase

struct ContentView: View {
    @State var scrollPosition = ScrollPosition()

    var body: some View {
        ScrollView { /* ... */ }
            .scrollPosition($scrollPosition)
            .overlay(alignment: .bottom) {
                Button {
                    scrollToShowcase()
                } label: { /* ... */ }
            }
    }

    func scrollToShowcase() {
        withAnimation {
            scrollPosition.scrollTo(id: "showcase-header")
        }
    }
}
Dynamic number of views swift · at 18:24 ↗
// Dynamic number of views

struct ContentView: View { /* ... */ }

struct StepView: View {
    let step: Step

    @Environment(\.detailLevel) var detailLevel

    var body: some View {
        if step.isVisible(in: detailLevel) { /* ... */ }
    }
}
Filter at the data level swift · at 18:53 ↗
// Filter at the data level

struct ContentView: View {
    @Query var steps: [Step]

    init(detailLevel: DetailLevel) {
        _steps = Query(filter: #Predicate<Step> { step in
            step.detailLevel >= detailLevel
        })
    }

    var body: some View { /* ... */ }
}

struct StepView: View { /* ... */ }
Using onGeometryChange in lazy stack subview swift · at 19:16 ↗
// Don't change layout after views appear

struct ContentView: View { /* ... */ }

struct StepView: View {
    let step: Step
    @State var subtitleHeight: CGFloat?

    var body: some View {
        VStack {
            StepDiagram(diagram: step.diagram)
                .frame(height: diagramHeight(subtitleHeight: subtitleHeight))
            Title(step.title)
            Subtitle(step.subtitle)
                .onGeometryChange(for: CGFloat.self, of: \.size.height) { _, value in
                    subtitleHeight = value
                }
        }
    }
}
Using custom layout in lazy stack subview swift · at 19:17 ↗
// Don't change layout after views appear

struct ContentView: View { /* ... */ }

struct StepView: View {
    let step: Step

    var body: some View {
        StepLayout {
            StepDiagram(diagram: step.diagram)
            Title(step.title)
            Subtitle(step.subtitle)
        }
    }
}

struct StepLayout: Layout { /* ... */ }

Resources