WWDC Quick Look 💓 By SwiftGGTeam
Demystify SwiftUI containers

Demystify SwiftUI containers

元の動画を見る

Highlight

SwiftUI に ForEach(subviewOf:)Group(subviewsOf:)ForEach(sectionOf:)ContainerValues API が追加され、任意のコンテンツ組み合わせ、グループ化、カスタム modifier に対応したコンテナビューを構築できるようになりました。

主要内容

SwiftUI の List はすでに柔軟なコンテンツ構成をサポートしています。静的ビューと ForEach で動的に生成されるビューを混在させ、Section でグループ化し、listRowSeparator() のような modifier で行スタイルをカスタマイズできます。しかし独自のコンテナビュー(カードグリッドやカンバンレイアウトなど)を構築したい場合、これまでデータ駆動のイニシャライザに依存するしかなく、柔軟性が制限されていました。

Apple は今年、3 つの新 API を導入してこの問題に対処しました。ForEach(subviewOf:) は任意のビューの「解決済みサブビュー」を走査します。静的に宣言されたものでも ForEach で動的に生成されたものでも対象です。Group(subviewsOf:) は解決済みサブビューのコレクションを一括取得し、数のカウントや一括処理が必要な場面に適しています。ForEach(sectionOf:) はビュー内の Section を検出して走査し、カスタムコンテナでも List と同様のグループ化をサポートします。ContainerValues と組み合わせることで、コンテナはサブビューに設定されたカスタム値を読み取り、listRowSeparator() のようなコンテナ固有の modifier を実現できます。

この API 群の鍵は「宣言ビュー」と「解決済みビュー」の違いを理解することです。宣言ビューはコードに書く ViewBuilder コンテンツで、解決済みビューは SwiftUI が実行時に実際にレンダリングするビューです。ForEach はデータに基づいて複数のビューに展開され、if 文は条件に応じてレンダリングするかどうかを決定します。新 API はすべて解決済みビューを操作するため、コンテナ実装がよりシンプルになります。

詳細

基本的なコンテナ実装

データ駆動の DisplayBoard コンテナ(05:27):

struct DisplayBoard<Data: Collection, Content: View>: View {
  var data: Data
  @ViewBuilder var content: (Data.Element) -> Content

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

キーポイント:

  • data コレクションは @ViewBuilder クロージャでビューにマッピング
  • ForEach がデータを反復して CardView を生成
  • コンテナはこの単一のデータ駆動パターンのみサポート

任意のコンテンツ組み合わせのサポート

ViewBuilder と ForEach(subviewOf:) を使ったリファクタリング(05:27):

struct DisplayBoard<Content: View>: View {
  @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(songsFromSam) { song in
    Text(song.title)
  }
}

キーポイント:

  • データ駆動パラメータを削除し、単一の @ViewBuilder content に変更
  • ForEach(subviewOf:) が「解決済みサブビュー」を走査(静的・動的どちらも)
  • コンテナ実装を変更せずに静的ビューと ForEach を混在可能

宣言ビュー vs 解決済みビュー

SwiftUI のビュー解決プロセスの理解(08:00):

// 1 つの宣言ビュー
ForEach(songsFromSam) { song in
  Text(song.title)
}

// 9 つの解決済みサブビュー
Text("I Container Multitudes")
...
Text("Love Stack")

キーポイント:

  • ForEach は視覚的な外観を持たない宣言ビュー
  • データから複数の解決済みサブビューを生成する役割
  • Group はコンテンツを直接解決済みサブビューに展開
  • EmptyView はゼロのサブビューに解決
  • if 文は条件に応じて解決結果を決定

サブビューの一括処理

Group(subviewsOf:) でサブビュー数をカウント(10:00):

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

キーポイント:

  • Group(subviewsOf:) が解決済みサブビューのコレクションを取得
  • count などのプロパティでコレクション情報にアクセス可能
  • サブビュー数に応じてレイアウトを動的調整する場面に適している

Section グループ化のサポート

ForEach(sectionOf:) で Section を走査(11:42):

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() }
}

キーポイント:

  • ForEach(sectionOf:) がコンテンツ内の Section を検出して走査
  • 各 section に headercontent プロパティ
  • header.isEmpty でヘッダーコンテンツの有無を確認
  • 各 Section に独立したレイアウトを作成可能

カスタムコンテナ modifier

ContainerValues でコンテナ固有の modifier を定義(14:46):

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

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

キーポイント:

  • ContainerValues は新しいキーバリューストレージ型
  • @Entry マクロで新しいコンテナ値エントリを宣言
  • containerValue() modifier で値を設定

コンテナ内でコンテナ値を読み取る(15:42):

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
          }
        }
      }
    }
  }
}

キーポイント:

  • サブビューに containerValues プロパティで値を読み取り
  • Section にも containerValues がある
  • Section に適用した modifier はすべてのサブビューに影響

カスタム modifier の使用(16:15):

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)
    }
  }
}

キーポイント:

  • modifier は個別のビューに適用可能
  • ForEach 内の各ビューにも適用可能
  • Section に適用するとすべてのサブビューに影響

重要ポイント

  • カードグリッドコンテナの構築: ForEach(subviewOf:) を使って DisplayBoard のようなカードグリッドコンテナを構築し、静的カードと動的カードの混在配置をサポートします。ViewBuilder 出力を手動で解析するより信頼性が高く、SwiftUI の宣言的更新メカニズムにも適応しやすいです。まずシンプルなグリッドレイアウトから始め、サブビュー走査ロジックを検証してから機能を拡張しましょう。

  • カンバンレイアウトの列グループ化: ForEach(sectionOf:) でカンバンレイアウトに列グループ化を追加します。各 Section が 1 列に対応し、Section header が列タイトルになります。列を表す独自のデータモデルを定義するより直感的で、ユーザーは SwiftUI の Section API で直接コンテンツを整理でき、学習コストが下がります。

  • コンテナへの装飾 modifier の追加: ContainerValues でグリッドアイテムの列跨ぎ、カードの強調スタイル、特定のインタラクションの無効化など、コンテナ固有の装飾オプションを追加します。これらの modifier はコンテナと強く結合しており、Environment や Preference には不向きです。ContainerValues はまさにこの用途のために設計されており、値は直接のコンテナにのみ可視で、ビュー階層に漏れません。

関連セッション

コメント

GitHub Issues · utterances