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 ↗Chapters
Code shown on screen · 26 snippets
Origami app
// Origami app
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(steps) { step in
StepView(step: step)
}
}
}
}
}
struct StepView: View { /* ... */ } Horizontally scrolling showcase
// 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
// 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
// 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
// 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
// Absolute offset
struct ContentView: View {
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
// Absolute offset
struct ContentView: View {
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
// Origami
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(steps) { step in
StepView(step: step)
}
}
}
}
}
struct StepView: View { /* ... */ } Multiple resolved subviews
// Multiple subviews
struct ContentView: View { /* ... */ }
struct StepView: View {
let step: Step
var body: some View {
StepDiagram(/* ... */)
StepInstructions(/* ... */)
}
} Dynamic number of subviews
// Dynamic number of views
struct ContentView: View { /* ... */ }
struct StepView: View {
let step: Step
(\.detailLevel) var detailLevel
var body: some View {
if step.isVisible(in: detailLevel) {
VStack { /* ... */ }
}
}
} Filtering on the view level
// Dynamic number of views
struct ContentView: View { /* ... */ }
struct StepView: View {
let step: Step
(\.detailLevel) var detailLevel
(\.writingStyle) var writingStyle
var body: some View {
if step.isVisible(in: detailLevel) { /* ... */ }
}
} Filtering on the data level
// Filter at the data level
struct ContentView: View {
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
// Optional unwrapping
struct ContentView: View { /* ... */ }
struct StepView: View {
let step: Step
(\.apiToken) var token
var body: some View {
if let token { /* ... */ }
}
} Optional unwrapping
// Optional unwrapping
struct ContentView: View { /* ... */ }
struct StepView: View {
let step: Step
(NetworkClient.self) var networkClient
var body: some View { /* ... */ }
} Loading more content
// Loading more content
struct Showcase: View {
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
// onAppear
struct StepView: View {
let id: Step.ID
var viewModel = StepViewModel()
var body: some View {
VStack {
if let content = viewModel.content { /* ... */ }
}
.onAppear {
viewModel.configure(with: id)
}
}
} Lazy stack subview ready before onAppear
// onAppear
struct StepView: View {
var viewModel: StepViewModel
init(id: Step.ID) {
_viewModel = State(initialValue: StepViewModel(id: id))
}
var body: some View { /* ... */ }
} Loading diagram with task modifier
// Diagram loading
struct StepView: View {
let step: Step
var diagramLoader = DiagramLoader()
var diagram: Diagram?
var body: some View {
VStack { /* ... */ }
.task {
diagram = await diagramLoader.loadDiagram(id: step.id)
}
}
} Loading diagram in initializer
// Diagram loading
struct StepView: View {
let step: Step
var diagramLoader: DiagramLoader
init(step: Step) {
self.step = step
_diagramLoader = State(initialValue: DiagramLoader(id: step.id))
}
var body: some View { /* ... */ }
}
class DiagramLoader { /* ... */ } Highlight @State variable
// Highlighting
struct ContentView: View { /* ... */ }
struct StepView: View {
let step: Step
var isHighlighted = false
var body: some View { /* ... */ }
} Highlight @Binding
// Highlighting
struct ContentView: View {
var highlighted: Set<Step.ID> = []
var body: some View { /* ... */ }
}
struct StepView: View {
let step: Step
var highlighted: Set<Step.ID>
var body: some View { /* ... */ }
} Programmatically scroll to showcase
// Programmatically scroll to showcase
struct ContentView: View {
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
// Dynamic number of views
struct ContentView: View { /* ... */ }
struct StepView: View {
let step: Step
(\.detailLevel) var detailLevel
var body: some View {
if step.isVisible(in: detailLevel) { /* ... */ }
}
} Filter at the data level
// Filter at the data level
struct ContentView: View {
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
// Don't change layout after views appear
struct ContentView: View { /* ... */ }
struct StepView: View {
let step: Step
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
// 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
Related sessions
-
15 min -
19 min -
27 min