View in English

  • Apple 开发者
    • 入门汇总

    探索“入门汇总”

    • 概览
    • 学习
    • Apple Developer Program

    及时了解最新动态

    • 最新动态
    • 开发者你好
    • 平台

    探索“平台”

    • Apple 平台
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    • App Store

    精选

    • 设计
    • 分发
    • 游戏
    • 配件
    • 网页
    • Home
    • CarPlay 车载
    • 技术

    探索“技术”

    • 概览
    • Xcode
    • Swift
    • SwiftUI

    精选

    • 辅助功能
    • App Intents
    • Apple 智能
    • 游戏
    • 机器学习与 AI
    • 安全性
    • Xcode Cloud
    • 社区

    探索“社区”

    • 概览
    • “与 Apple 会面交流”活动
    • 社区主导的活动
    • 开发者论坛
    • 开源

    精选

    • WWDC
    • Swift Student Challenge
    • 开发者故事
    • App Store 大奖
    • Apple 设计大奖
    • Apple Developer Centers
    • 文档

    探索“文档”

    • 文档库
    • 技术概述
    • 示例代码
    • 《人机界面指南》
    • 视频

    发布说明

    • 精选更新
    • iOS
    • iPadOS
    • macOS
    • watchOS
    • visionOS
    • Apple tvOS
    • Xcode
    • 下载

    探索“下载”

    • 所有下载
    • 操作系统
    • 应用程序
    • 设计资源

    精选

    • Xcode
    • TestFlight
    • 字体
    • SF Symbols
    • Icon Composer
    • 支持

    探索“支持”

    • 概览
    • 帮助指南
    • 开发者论坛
    • “反馈助理”
    • 联系我们

    精选

    • 《开发者账户帮助》
    • 《App 审核指南》
    • 《App Store Connect 帮助》
    • 即将实行的要求
    • 协议和准则
    • 系统状态
  • 快速链接

    • 活动
    • 新闻
    • 论坛
    • 示例代码
    • 视频
 

视频

打开菜单 关闭菜单
  • 专题
  • 所有视频
  • 关于

更多视频

  • 简介
  • 概要
  • 代码
  • 深入探索 SwiftUI 的惰性堆栈与滚动

    了解 SwiftUI 惰性堆栈的内部工作原理。我们将探索 LazyVStack 和 LazyHStack 如何估算大小、惰性加载子视图并预取内容,从而提供流畅的滚动浏览体验。我们还将介绍高级性能优化方式、状态管理最佳做法,以及实现精准程序化滚动的技巧。为了充分从这个讲座中获益,我们建议你先熟悉一下使用堆栈的 SwiftUI 布局。

    章节

    • 0:00 - Introduction
    • 1:24 - Layout
    • 9:13 - Subview loading
    • 13:15 - Prefetching
    • 17:40 - Programmatic scrolling
    • 19:55 - Next steps

    资源

    • Grouping data with lazy stack views
      • 高清视频
      • 标清视频

    相关视频

    WWDC26

    • 跟随编程:使用 SwiftUI 构建强大的拖放功能

    WWDC22

    • 使用 SwiftUI 构建自定布局

    WWDC20

    • SwiftUI 中的叠放、网格和大纲
  • 搜索此视频…
    • 1:23 - Origami app

      // Origami app
      
      struct ContentView: View {
          var body: some View {
              ScrollView {
                  LazyVStack {
                      ForEach(steps) { step in
                          StepView(step: step)
                      }
                  }
              }
          }
      }
      
      struct StepView: View { /* ... */ }
    • 5:11 - 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)
                      }
                  }
              }
          }
      }
    • 6:30 - 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: { /* ... */ }
          }
      }
    • 7:04 - 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: { /* ... */ }
          }
      }
    • 7:36 - 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: { /* ... */ }
          }
      }
    • 8:20 - Scroll to Showcase button

      // 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
                  }
          }
      }
    • 8:51 - Scroll to Showcase button

      // 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)
                  }
          }
      }
    • 9:29 - One resolved subview

      // Origami
      
      struct ContentView: View {
          var body: some View {
              ScrollView {
                  LazyVStack {
                      ForEach(steps) { step in
                          StepView(step: step)
                      }
                  }
              }
          }
      }
      
      struct StepView: View { /* ... */ }
    • 10:03 - Multiple resolved subviews

      // Multiple subviews
      
      struct ContentView: View { /* ... */ }
      
      struct StepView: View {
          let step: Step
      
          var body: some View {
              StepDiagram(/* ... */)
              StepInstructions(/* ... */)
          }
      }
    • 10:52 - Dynamic number of subviews

      // 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 { /* ... */ }
              }
          }
      }
    • 11:46 - Filtering on the view level

      // 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) { /* ... */ }
          }
      }
    • 12:15 - Filtering on the data level

      // 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 { /* ... */ }
    • 12:35 - Optional unwrapping

      // Optional unwrapping
      
      struct ContentView: View { /* ... */ }
      
      struct StepView: View {
          let step: Step
      
          @Environment(\.apiToken) var token
      
          var body: some View {
              if let token { /* ... */ }
          }
      }
    • 12:48 - Optional unwrapping

      // Optional unwrapping
      
      struct ContentView: View { /* ... */ }
      
      struct StepView: View {
          let step: Step
      
          @Environment(NetworkClient.self) var networkClient
      
          var body: some View { /* ... */ }
      }
    • 15:28 - Loading more content

      // 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()
                      }
              }
          }
      }
    • 15:53 - Setting up lazy stack subview in onAppear

      // 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)
              }
          }
      }
    • 16:14 - Lazy stack subview ready before onAppear

      // onAppear
      
      struct StepView: View {
          @State var viewModel: StepViewModel
      
          init(id: Step.ID) {
              _viewModel = State(initialValue: StepViewModel(id: id))
          }
      
          var body: some View { /* ... */ }
      }
    • 16:23 - Loading diagram with task modifier

      // 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)
                  }
          }
      }
    • 16:40 - Loading diagram in initializer

      // 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 { /* ... */ }
    • 17:16 - Highlight @State variable

      // Highlighting
      
      struct ContentView: View { /* ... */ }
      
      struct StepView: View {
          let step: Step
          @State var isHighlighted = false
      
          var body: some View { /* ... */ }
      }
    • 17:33 - Highlight @Binding

      // 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 { /* ... */ }
      }
    • 17:58 - Programmatically scroll to showcase

      // 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")
              }
          }
      }
    • 18:24 - Dynamic number of views

      // 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) { /* ... */ }
          }
      }
    • 18:53 - Filter at the data level

      // 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 { /* ... */ }
    • 19:16 - Using onGeometryChange in lazy stack subview

      // 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
                      }
              }
          }
      }
    • 19:17 - 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 { /* ... */ }
    • 0:00 - Introduction
    • Rens Breur gives an introduction to lazy stacks, an essential SwiftUI component for long and custom scrolling content.

    • 1:24 - Layout
    • How LazyVStack and LazyHStack lay out their subviews: only visible views are added, and the full size of lazy stacks is estimated. See how the lazy stack handles these estimated sizes, how the estimations can change, and how it coordinates the estimated content offset with the embedding ScrollView. Lazy stacks can also be composed to create more complex layouts.

    • 9:13 - Subview loading
    • How view structs are resolved into the individual subviews that the lazy stack sees — the 1-to-1 mapping you might expect isn't always what happens. A view's body can resolve to multiple subviews or to a dynamic number of subviews, which has consequences for what the lazy stack keeps alive.

    • 13:15 - Prefetching
    • Lazy stacks prefetch subviews before they scroll on screen, performing partial render work to avoid hitches. To take advantage of this, don't delay lazy stack subview set-up to onAppear. Lazy stack subviews are kept around a little longer after they are scrolled out of screen but are removed eventually. Move state that must survive being scrolled off screen into model objects or bindings from outer views.

    • 17:40 - Programmatic scrolling
    • Using a ScrollPosition binding to scroll to a target view works even when the target is off-screen, with the lazy stack estimating its position. Same pitfalls apply: dynamic subview counts in a ForEach hurt scroll performance, and layout passes driven by onAppear or onGeometryChange make scrolling less smooth. Sometimes a custom Layout is the better solution.

    • 19:55 - Next steps
    • Avoid absolute content size and offset with lazy stacks, don't filter data with conditional view content in leaf views, set up views in init rather than onAppear, and keep important state outside view structs that may scroll off screen.

Developer Footer

  • 视频
  • WWDC26
  • 深入探索 SwiftUI 的惰性堆栈与滚动
  • 打开菜单 关闭菜单
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    打开菜单 关闭菜单
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    打开菜单 关闭菜单
    • 辅助功能
    • 配件
    • Apple 智能
    • App 扩展
    • App Store
    • 音频与视频 (英文)
    • 增强现实
    • 设计
    • 分发
    • 教育
    • 字体 (英文)
    • 游戏
    • 健康与健身
    • App 内购买项目
    • 本地化
    • 地图与位置
    • 机器学习与 AI
    • 开源资源 (英文)
    • 安全性
    • Safari 浏览器与网页 (英文)
    打开菜单 关闭菜单
    • 完整文档 (英文)
    • 部分主题文档 (简体中文)
    • 教程
    • 下载
    • 论坛 (英文)
    • 视频
    打开菜单 关闭菜单
    • 支持文档
    • 联系我们
    • 错误报告
    • 系统状态 (英文)
    打开菜单 关闭菜单
    • Apple 开发者
    • App Store Connect
    • 证书、标识符和描述文件 (英文)
    • 反馈助理
    打开菜单 关闭菜单
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program (英文)
    • Mini Apps Partner Program
    • News Partner Program (英文)
    • Video Partner Program (英文)
    • 安全赏金计划 (英文)
    • Security Research Device Program (英文)
    打开菜单 关闭菜单
    • 与 Apple 会面交流
    • Apple Developer Center
    • App Store 大奖 (英文)
    • Apple 设计大奖
    • Apple Developer Academies (英文)
    • WWDC
    阅读最近新闻。
    获取 Apple Developer App。
    版权所有 © 2026 Apple Inc. 保留所有权利。
    使用条款 隐私政策 协议和准则