View in English

  • Apple Developer
    • 시작하기

    시작하기 탐색

    • 개요
    • 알아보기
    • Apple Developer Program

    알림 받기

    • 최신 뉴스
    • Hello Developer
    • 플랫폼

    플랫폼 탐색

    • Apple 플랫폼
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store

    피처링

    • 디자인
    • 배포
    • 게임
    • 액세서리
    • 웹
    • 홈
    • CarPlay
    • 기술

    기술 탐색

    • 개요
    • Xcode
    • Swift
    • SwiftUI

    피처링

    • 손쉬운 사용
    • 앱 인텐트
    • Apple Intelligence
    • 게임
    • 머신 러닝 및 AI
    • 보안
    • Xcode Cloud
    • 커뮤니티

    커뮤니티 탐색

    • 개요
    • Apple과의 만남 이벤트
    • 커뮤니티 주도 이벤트
    • 개발자 포럼
    • 오픈 소스

    피처링

    • WWDC
    • Swift Student Challenge
    • 개발자 이야기
    • App Store 어워드
    • Apple 디자인 어워드
    • 문서

    문서 탐색

    • 문서 라이브러리
    • 기술 개요
    • 샘플 코드
    • 휴먼 인터페이스 가이드라인
    • 비디오

    릴리즈 노트

    • 피처링 업데이트
    • iOS
    • iPadOS
    • macOS
    • watchOS
    • visionOS
    • tvOS
    • Xcode
    • 다운로드

    다운로드 탐색

    • 모든 다운로드
    • 운영 체제
    • 애플리케이션
    • 디자인 리소스

    피처링

    • Xcode
    • TestFlight
    • 서체
    • SF Symbols
    • Icon Composer
    • 지원

    지원 탐색

    • 개요
    • 도움말
    • 개발자 포럼
    • 피드백 지원
    • 문의하기

    피처링

    • 계정 도움말
    • 앱 심사 지침
    • 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
      • HD 비디오
      • SD 비디오

    관련 비디오

    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
    • tvOS
    • visionOS
    • watchOS
    메뉴 열기 메뉴 닫기
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    메뉴 열기 메뉴 닫기
    • 손쉬운 사용
    • 액세서리
    • Apple Intelligence
    • 앱 확장 프로그램
    • App Store
    • 오디오 및 비디오(영문)
    • 증강 현실
    • 디자인
    • 배포
    • 교육
    • 서체(영문)
    • 게임
    • 건강 및 피트니스
    • 앱 내 구입
    • 현지화
    • 지도 및 위치
    • 머신 러닝 및 AI
    • 오픈 소스(영문)
    • 보안
    • Safari 및 웹(영문)
    메뉴 열기 메뉴 닫기
    • 문서(영문)
    • 튜토리얼
    • 다운로드
    • 포럼(영문)
    • 비디오
    메뉴 열기 메뉴 닫기
    • 지원 문서
    • 문의하기
    • 버그 보고
    • 시스템 상태(영문)
    메뉴 열기 메뉴 닫기
    • Apple Developer
    • 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 Bounty Program(영문)
    • Security Research Device Program(영문)
    메뉴 열기 메뉴 닫기
    • Apple과의 만남
    • Apple Developer Center
    • App Store 어워드(영문)
    • Apple 디자인 어워드
    • Apple Developer Academy(영문)
    • WWDC
    최신 뉴스 읽기.
    Apple Developer 앱 받기.
    Copyright © 2026 Apple Inc. 모든 권리 보유.
    약관 개인정보 처리방침 계약 및 지침