SwiftUI の ViewThatFits で利用可能なスペースに適応する

iOS 16 で導入された ViewThatFits は、複数のレイアウト候補を用意しておき、利用可能なスペースに収まる最初のビューを自動的に選択するコンテナです。画面サイズや Dynamic Type の変化に応じてレイアウトを切り替えたいとき、GeometryReader より宣言的に書けるのが利点になります。

基本の使い方

ViewThatFits はクロージャ内に列挙したビューを上から順にチェックし、親のスペースに収まる最初のビューを表示します。

ViewThatFits {
    // 第1候補: 横並び
    HStack(spacing: 12) {
        Image(systemName: "star.fill")
        Text("お気に入りに追加")
        Spacer()
        Button("追加") { }
            .buttonStyle(.borderedProminent)
    }
    
    // 第2候補: 縦並び
    VStack(spacing: 8) {
        HStack {
            Image(systemName: "star.fill")
            Text("お気に入りに追加")
        }
        Button("追加") { }
            .buttonStyle(.borderedProminent)
    }
    
    // 第3候補: アイコンのみ
    Button { } label: {
        Image(systemName: "star.fill")
    }
    .buttonStyle(.borderedProminent)
}

画面幅が十分なら横並びレイアウト、狭くなると縦並び、さらに狭いとアイコンだけのボタンに自動で切り替わります。候補は上から順に評価されるため、最も理想的なレイアウトを最初に書くのがポイントです。

軸を指定する

ViewThatFits は水平と垂直の両方でフィットを判定しますが、in パラメータで判定軸を限定できます。

ViewThatFits(in: .horizontal) {
    Text("これは長いテキストで横幅を多く使います")
    Text("短いテキスト")
    Text("短")
}

.horizontal を指定すると、水平方向のスペースだけで判定が行われます。垂直方向がはみ出してもフィットしたと見なされるため、幅だけに応じた切り替えが実現できます。

ViewThatFits(軸指定なし)

水平・垂直の両方でスペースに収まるかを判定する。どちらかの軸ではみ出すと次の候補に進む

ViewThatFits(in: .horizontal)

水平方向のみで判定する。高さは無視されるため、幅だけに応じたレイアウト切り替えが可能

Dynamic Type への対応

ViewThatFits が特に威力を発揮するのは、Dynamic Type(文字サイズの変更)への対応です。ユーザーがシステム設定でフォントサイズを大きくすると、通常のレイアウトに収まらなくなることがあります。

ViewThatFits {
    HStack {
        Label("設定", systemImage: "gear")
        Spacer()
        Text("一般")
            .foregroundColor(.secondary)
        Image(systemName: "chevron.right")
            .foregroundColor(.secondary)
    }
    
    VStack(alignment: .leading) {
        Label("設定", systemImage: "gear")
        HStack {
            Text("一般")
                .foregroundColor(.secondary)
            Spacer()
            Image(systemName: "chevron.right")
                .foregroundColor(.secondary)
        }
    }
}

標準的なフォントサイズでは横並び、アクセシビリティサイズでは自動的に縦並びに切り替わります。GeometryReader でブレークポイントを計算するよりもずっと簡潔に書けるのが魅力です。

GeometryReader との使い分け

ViewThatFits と GeometryReader はどちらもレスポンシブレイアウトに使えますが、適材適所があります。

ViewThatFits は「複数の完成形をあらかじめ用意し、スペースに応じて切り替える」パターンに向いています。一方、GeometryReader は「正確な数値を取得して計算に使う」パターンに向いています。

// ViewThatFits が適しているケース
// → レイアウトのバリエーションが数パターンに限られる
ViewThatFits {
    WideLayout()
    NarrowLayout()
    MinimalLayout()
}

// GeometryReader が適しているケース
// → サイズに応じて連続的に値を変えたい
GeometryReader { proxy in
    let columns = Int(proxy.size.width / 120)
    // columns に応じた処理
}

明確なバリエーションがあるなら ViewThatFits、連続的なサイズ計算が必要なら GeometryReader を選びましょう。両方を組み合わせることも可能で、GeometryReader で大まかなサイズ区分を判定し、その中で ViewThatFits を使ってさらに細かい調整を行うといったアプローチも取れます。