• SwiftUI는 이미 여러가지의 컨테이너를 제공한다.

    • List
      • 정적 컨텐츠

        List {
          Text("Scrolling in the Deep")
          Text("Born to Build & Run")
          Text("Some Body Like View")
        }
        
      • 동적 컨텐츠

        List {
          Text("Scrolling in the Deep")
          Text("Born to Build & Run")
          Text("Some Body Like View")
        
          ForEach(otherSongs) { song in
            Text(song.title)
          }
        }
        
      • 어떤 형태의 컨텐츠던 하나의 컨테이너 안에 넣을 수 있다.

    • 일부 컨테이너는 심화 기능을 제공한다.
      • 헤더와 푸터를 가진 섹션으로 그룹화하기

        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)
            }
          }
        }
        
      • 컨테이너용 modifier

          Section("Other Songs") {
            ForEach(otherSongs) { song in
              Text(song.title)
                .listRowSeparator(.hidden)
            }
          }
        
  • 이 세션에서는 새로운 API를 통해서 이러한 기능들을 모두 지원하는 커스텀 컨테이너를 만드는 것을 다룬다.

  • 시작

    • 부르고 싶은 노래를 보여줄 게시판 뷰

    • 커스텀 카드 레이아웃을 통해서 랜덤한 위치에 카드를 배치하게 했다.

      스크린샷 2024-06-24 오후 12.22.00.png

      @State private var songs: [Song] = [
        Song("Scrolling in the Deep"),
        Song("Born to Build & Run"),
        Song("Some Body Like View"),
      ]
      
      var body: some View {
        DisplayBoard(songs) { song in
          Text(song.title)
        }
      }
      
      // DisplayBoard 구현
      var data: Data
      @ViewBuilder var content: (Data.Element) -> Content
      
      var body: some View {
        DisplayBoardCardLayout {
          ForEach(data) { item in
            CardView {
              content(item)
            }
          }
        }
        .background { BoardBackgroundView() }
      }
      
    • 지금은 단일 콜렉션만 보여줄 수 있어서 제한적이다.

      • 더 많고 다양한 합성을 지원하도록 하고 싶다.
  • Composition

    • SwiftUI의 리스트는 콜렉션을 직접 받을수도 있지만 다른 방법으로도 초기화를 할 수 있다.

      // 콜렉션을 받아서 초기화하기
      List(songsFromSam) { song in
        Text(song.title)
      }
      
      // 컨텐츠를 직접 명시해서 초기화하기
      List {
        Text("Scrolling in the Deep")
        Text("Born to Build & Run")
        Text("Some Body Like View")
      }
      
    • SwiftUI는 여러개의 컨텐츠를 합칠 수 있는 기능을 제공함으로써 이 간극을 메운다.

      • 예를 들어 data-driven 리스트를 ForEach를 사용해서 재작성할 수 있다.

      • 기존과 동작은 같지만 ForEach뷰는 viewbuilder안에 중첩될 수 있다.

        List {
          ForEach(songsFromSam) { song in
            Text(song.title)
          }
        }
        
    • 두 리스트의 컨텐츠를 뷰 만으로 표현할 수 있게 되면 두 리스트를 합친 리스트를 만들 수도 있게 된다.

      List {
        Text("Scrolling in the Deep")
        Text("Born to Build & Run")
        Text("Some Body Like View")
      
        ForEach(songsFromSam) { song in
          Text(song.title)
        }
      } 
      
    • DisplayBoard 컨테이너에도 같은 기능을 추가하고 싶다.

      • 데이터를 받던 것을 뷰 빌더만 받도록한다.

        @ViewBuilder var content: Content
        
        var body: some View {
          DisplayBoardCardLayout {
            ForEach(data) { item in
              CardView {
                content(item)
              }
            }
          }
          .background { BoardBackgroundView() }
        }
        
        • 다음으로 이 content를 사용하도록 해야 하는데, 여기서 ForEach(subviewOf:)라는 새로운 API를 용한다.

          • 단일 뷰를 입력으로 받아서 서브뷰를 trailing viewbuilder에 넘겨준다.
          @ViewBuilder var content: Content
          
          var body: some View {
            DisplayBoardCardLayout {
              ForEach(subviewOf: content) { subview in
                CardView {
                  subview
                }
              }
            }
            .background { BoardBackgroundView() }
          }
          
    • 덕분에 이제 DisplayBoard도 리스트처럼 구현할 수 있다.

      DisplayBoard {
        Text("Scrolling in the Deep")
        Text("Born to Build & Run")
        Text("Some Body Like View")
      
        ForEach(songsFromSam) { song in
          Text(song.title)
        }
      }
      
    • 어떻게 동작하는가? ForEach(subviewOf:) 에서 말하는 subview란?

      • 다른 뷰 안쪽에 있는 뷰

      • 하지만 ForEach같은 경우는 여러개의 subView를 나타낸다.

        스크린샷 2024-06-24 오후 12.45.56.png

      • 리스트에서도 마찬가지다.

        스크린샷 2024-06-24 오후 12.46.29.png

      • 이 두가지를 구분하는 게 중요하다.

        • 선언된 subview: 코드상에서 나타나는 subview

          스크린샷 2024-06-24 오후 12.47.10.png

        • 결정된 subview: 실제로 화면에 보여지는 subview

          스크린샷 2024-06-24 오후 12.49.20.png

    • SwiftUI의 선언적 시스템에서는 선언된 subView는 결정된 subview를 만드는 레시피 역할을 한다.

      • ForEach는 특별한 시각적인 영향이나 개별 동작은 없지만 목적 자체가 결정된 subview콜렉션을 만드는 역할을 한다.

        스크린샷 2024-06-24 오후 12.51.58.png

      • 내장 컨테이너인 Group도 비슷한 역할을 한다.

        스크린샷 2024-06-24 오후 12.52.41.png

      • 결정된 subview가 없는 경우도 있다.(EmptyView)

        스크린샷 2024-06-24 오후 12.53.25.png

      • 조건에 따라서 다르게 결정하는 경우도 있다.(if 분기)

        스크린샷 2024-06-24 오후 12.54.02.png

    • ForEach(subviewOf:)는 결정된 subview들을 순회하면서 커스텀 컨테이너가 가능한 모든 조합을 더 적은 코드로 지원할 수 있게 해준다.

      • subview결정은 SwiftUI가 해준다.
    • 더 유연해졌기 때문에 또 다른 리스트를 추가하는 것도 쉽다.

      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)
        }
      }
      
    • 카드가 너무 많으면 잘 안보이기 떄문에 이때는 크기를 줄이고 싶다.

      • 이때는 Group(subviewsOf:)를 사용해서 결정된 subview 콜렉션을 받아서 처리한다.

        @ViewBuilder var content: Content
        
        var body: some View {
          DisplayBoardCardLayout {
            Group(subviewsOf: content) { subviews in
              ForEach(subviews) { subview in
                CardView(
                  scale: subviews.count > 15 ? .small : .normal
                ) {
                  subview
                }
              }
            }
          }
          .background { BoardBackgroundView() }
        }
        
  • Sections

    • 내장 컨테이너 중 List는 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)
          }
        }
      }
      
    • Section은 그룹과 비슷하지만 헤더와 푸터 등 추가 메타데이터를 지원한다.

    • DisplayBoard에서도 Section을 지원하고 싶다.

      • 하지만 커스텀 컨테이는 기본적으로 섹션을 지원하지 않기 때문에 추가적인 작업이 필요하다.
      DisplayBoard {
        Section("Matt's Favorites") {
          Text("Scrolling in the Deep")
          Text("Born to Build & Run")
          Text("Some Body Like View")
        }
        Section("Sam's Favorites") {
          ForEach(songsFromSam) { song in
            Text(song.title)
          }
        }
        Section("Sommer's Favorites") {
          ForEach(songsFromSommer) { song in
            Text(song.title)
          }
        }
      }
      
    • section을 위한 뷰 따로 만들기

      @ViewBuilder var content: Content
      
      var body: some View {
        DisplayBoardSectionContent {
          content
        }
        .background { BoardBackgroundView() }
      }
      
      struct DisplayBoardSectionContent<Content: View>: View {
        @ViewBuilder var content: Content
        ...
      }
      
    • ForEach(sectionOf: ) API로 섹션 지원

      스크린샷 2024-06-24 오후 1.04.56.png

      @ViewBuilder var content: Content
      
      var body: some View {
        HStack(spacing: 80) {
          ForEach(sectionOf: content) { section in
            DisplayBoardSectionContent {
              section.content
            }
          }
        }
        .background { BoardBackgroundView() }
      }
      
    • 헤더 지원

      스크린샷 2024-06-24 오후 1.05.47.png

      @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

    • listRowSeparator modifier 같이 컨테이너 안의 컨텐츠를 커스텀하고 싶다.

    • 이런 modifier를 쓰더라도 실제로 이를 받아서 구현하는 건 컨테이너의 몫이다.

    • DisplayBoard에서는 특정 카드를 선택하지 않겠다는 것을 나태는 빗금 표시를 커스텀하고 싶다.

      스크린샷 2024-06-24 오후 1.09.52.png

    • 이러한 container용 값을 커스텀할 수 있는 Container Value라는 기능이 추가되었다.

    • modifier정의

    • 컨테이너에서 사용하기

    • 실제로 사용하기