Highlight
SwiftUI に
ForEach(subviewOf:)、Group(subviewsOf:)、ForEach(sectionOf:)、ContainerValuesAPI が追加され、任意のコンテンツ組み合わせ、グループ化、カスタム 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 に
headerとcontentプロパティ 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はまさにこの用途のために設計されており、値は直接のコンテナにのみ可視で、ビュー階層に漏れません。
関連セッション
- What’s new in SwiftUI — SwiftUI 年次アップデートのハイライト
- Advanced SwiftUI containers — より高度なコンテナ構築テクニック
- Meet SwiftUI for spatial computing — 空間コンピューティングにおける SwiftUI コンテナ
- Build accessible apps with SwiftUI — アクセシブルなアプリ構築時のコンテナ考慮事項
コメント
GitHub Issues · utterances