본문 바로가기
WWDC

WWDC24: SwiftUI 컨테이너 쉽게 이해하기

by songmoro 2024. 7. 2.
728x90

본문은 영어 공부를 겸해 WWDC 영상을 보고 정리한 것으로 오역이 있을 수 있습니다.

정확한 내용은 원문 참고 바랍니다.

 

원문


Overview

SwiftUI에서 커스텀 컨테이너 뷰를 만드는 새로운 방법 소개

 

 

컨테이너 뷰: 컨테이너 뷰는 클로저를 사용해 콘텐츠 랩핑

뷰 빌더에 정적 콘텐츠 정의 예: 하드 코딩 Text 뷰

뷰 빌더에 동적 콘텐츠 정의 예: ForEach 사용해서 Text 뷰

List {
    // 정적 콘텐츠 정의
    Text("Scrolling in the Deep")
    Text("Born to Build & Run")
    Text("Some body Like View")	
	
    // 동적 콘텐츠 정의
    ForEach(otherSongs) { song in
    	Text(song.title)
    }
}

Composition

Composition: Composition은 여러 개의 뷰를 결합하여 더 복잡한 UI를 구성하는 것을 의미(ChatGPT)

SwiftUI는 다양한 종류의 콘텐츠를 구성(composing)하기 위한 API를 제공한다.

 

예를 들어, 아래 data-driven list 코드를 ForEach를 사용하는 코드로 다시 작성할 수 있다.

두 코드는 기능적으론 동일하지만, ForEach 뷰는 뷰 빌더 내부에서 중첩 될 수 있다는 게 중요하다.

// data-driven list
List(songsFromSam) { song in
	Text(song.title)	
}

// -> 
// data-driven ForEach list
List {
  ForEach(songsFromsam) { song in
	  Text(song.title)
  }
}

즉, List 내부에서 뷰 만을 사용해 서로 다른 콘텐츠를 결합할 수 있다는 뜻이다.

 

아래 unified List는 composition의 예.

List {
    Text("Scrolling in the Deep")
    Text("Born to Build & Run")
    Text("Some body Like View")
    
    ForEach(otherSongs) { song in
    	Text(song.title)
    }
}

 

 

container에 유연한 composotion 지원하기: 새로운 API, ForEach(subviewOf:) 사용해서 리팩토링 이전 코드에서 단일 속성만 지원하던 것과 달리, 리팩토링한 코드는 다른 종류의 뷰도 수용할 수 있다.

var data: Data
@ViewBuilder var content: (Data.Element) -> Content

var body: some View {
    DisplayBoardCardLayout {
        ForEach(data) { item in
            CardView {
                content(item)
            }
        }
    }
    .background { BoardBackgroundView() }
}

->

@ViewBuilder var content: Content

var body: some View {
    DisplayBoardCardLayout {
        ForEach(subviewOf: content) { subview in
            CardView {
            	subview
            }
        }
    }
    .background { BoardBackgroundView() }
}
DisplayBoard {
    Text("Scrolling in the Deep")
    Text("Born to Build & Run")
    Text("Some body Like View")
    
    ForEach(otherSongs) { song in
    	Text(song.title)
    }
}

->
// 코드 수정 없이 바로 추가 가능
DisplayBoard {
    Text("Scrolling in the Deep")
    Text("Born to Build & Run")
    Text("Some Body Like View")

    ForEach(songsFromSam) { song in
        Text(song.title)
    }

    ForEach(songsFromSommer) { song in
        Text(song.title)
    }
}

 

subview: 다른 뷰를 포함하고 있는 뷰

Declared subview: 명시적으로 선언된 서브 뷰

Resolved subview: 앱이 실행될 때(런타임 타임) 생성되는 실질적인 뷰

// declared subviews
Group {
    Text("Scrolling in the Deep")
    Text("Born to Build & Run")	
    Text("Some body Like View")	
}

// ->
// resolved subviews
Text("Scrolling in the Deep")
Text("Born to Build & Run")	
Text("Some body Like View")

 

 

ForEach(subviewOf:): 콘텐츠의 resolved subview만 반복. 따라서, 적은 코드로 컨테이너가 composition을 가능하게 만들 수 있다.

 

 

Sections

리스트는 built-in 컨테이너의 예로 section을 지원한다.

List {
    Section("Favorite Songs") {
        Text("Scrolling in the Deep")
        Text("Born to Build & Run")
        Text("Some Body Like View")
    }

    Section("Other Songs") {
        ForEach(otherSongs) { song in
            Text(song.title)
        }
    }
}

 

 

ForEach(sectionOf:): 커스텀 컨테이너에 header, footer를 지원할 수 있게 해준다.

@ViewBuilder var content: Content

var body: some View {
  HStack(spacing: 80) {
    ForEach(sectionOf: content) { section in
      VStack(spacing: 20) {
        if !section.header.isEmpty {
          DisplayBoardSectionHeaderCard { section.header }
        } 
        DisplayBoardSectionContent {
          section.content
        }
        .background { BoardSectionBackgroundView() }
      }
    }
  }
  .background { BoardBackgroundView() }
}

 

Customization

container-specific modifier인 새로운 API 지원, container values

Container values: keyed storage, Environment와 Preferences 같은 컨셉.

뷰 계층의 아래로 전달하는 environment values, 위로 전달하는 Preferences와 다르게 컨테이너 안에서만 유효한 값이다.

 

뷰 계층도:

 

Environment values 계층도:

 

Preference values 계층도:

 

Container values 계층도:

 

 

사용 방법:

extension ContainerValues {
  @Entry var isDisplayBoardCardRejected: Bool = false
}

extension View {
  func displayBoardCardRejected(_ isRejected: Bool) -> some View {
    containerValue(\\.isDisplayBoardCardRejected, isRejected)
  }
}

struct DisplayBoardSectionContent<Content: View>: View {
  @ViewBuilder var content: Content

  var body: some View {
    DisplayBoardCardLayout {
      Group(subviewsOf: content) { subviews in
        ForEach(subviews) { subview in
          let values = subview.containerValues
          CardView(
            scale: (subviews.count > 15) ? .small : .normal,
            isRejected: values.isDisplayBoardCardRejected
          ) {
            subview
          }
        }
      }
    }
  }
}

 

DisplayBoard {
  Section("Matt's Favorites") {
    Text("Scrolling in the Deep")
      .displayBoardCardRejected(true)
    Text("Born to Build & Run")
    Text("Some Body Like View")
  }
  Section("Sam's Favorites") {
    ForEach(songsFromSam) { song in
      Text(song.title)
        .displayBoardCardRejected(song.samHasDibs)
    }
  }
  Section("Sommer's Favorites") {
    ForEach(songsFromSommer) { Text($0.title) }}}
  }
  .displayBoardCardRejected(true)
}

 

728x90