SwiftUI @Binding で子ビューに状態を渡す

親ビューが @State で保持する値を子ビューから読み書きしたい場面は頻繁にある。たとえば、設定画面のトグルスイッチを独立したコンポーネントとして切り出すケースだ。このとき使うのが @Binding で、親の状態への「参照」を子ビューに渡す役割を果たす。

@Binding の基本

@Binding は値そのものを所有しない。あくまで別の場所にある値への双方向の接続を提供するプロパティラッパーだ。子ビューが @Binding の値を変更すると、その変更は元の @State に反映され、親ビューも再描画される。

struct ParentView: View {
    @State private var isOn = false

    var body: some View {
        VStack {
            Text(isOn ? "ON" : "OFF")
            ToggleRow(isOn: $isOn)
        }
    }
}

struct ToggleRow: View {
    @Binding var isOn: Bool

    var body: some View {
        Toggle("スイッチ", isOn: $isOn)
    }
}

`isOn` でバインディングを渡し、子ビュー側では `@Binding var isOn: Bool` で受け取る。子ビューがトグルを切り替えると、親の `isOn` も即座に更新されて画面全体が整合性を保つ。 ## 記号の意味

@State プロパティの前に name) という書き方が自然にできるのは、この仕組みがあるからだ。

@Binding に private を付けない

@State には private を付けるのが慣例だが、@Binding には付けないのが一般的だ。理由は明快で、@Binding は親から値を受け取るためのインターフェースであり、外部からアクセスできなければ意味がない。

// 正しい: 外部から渡せる
struct ChildView: View {
    @Binding var score: Int
    var body: some View {
        Text("スコア: \(score)")
    }
}

// 間違い: private だと親から渡せない
struct ChildView: View {
    @Binding private var score: Int  // ← コンパイルエラー
    var body: some View {
        Text("スコア: \(score)")
    }
}

実践的な使用例

実際のアプリでは、フォームの入力部品やカスタムコントロールを部品化する際に @Binding が活躍する。以下は星評価コンポーネントの例で、タップした星の位置に応じて評価値を更新する。

struct StarRating: View {
    @Binding var rating: Int
    let maxRating: Int = 5

    var body: some View {
        HStack {
            ForEach(1...maxRating, id: \.self) { index in
                Image(systemName: index <= rating
                    ? "star.fill" : "star")
                    .foregroundColor(.yellow)
                    .onTapGesture {
                        rating = index
                    }
            }
        }
    }
}

このコンポーネントは @Binding var rating: Int を持つだけなので、どの画面からでも再利用でき、評価値の管理責任は常に親ビューに残る。

Binding の手動生成

通常は $ で自動的にバインディングを取得するが、Binding(get:set:) を使って手動で生成することもできる。バリデーションや値の変換を挟みたい場合に有用だ。

struct ClampedSlider: View {
    @State private var rawValue: Double = 50

    var body: some View {
        let clamped = Binding<Double>(
            get: { rawValue },
            set: { rawValue = min(max($0, 0), 100) }
        )

        VStack {
            Slider(value: clamped, in: 0...100)
            Text("値: \(Int(rawValue))")
        }
    }
}

この例では set クロージャ内で値を 0〜100 にクランプしており、スライダーの操作が安全な範囲に制限される。

プレビューでの Binding

Xcode のプレビューでは @Binding を持つビューの表示に .constant() を使うことが多い。これは固定値のバインディングを生成するヘルパーで、プレビュー専用のコードとして割り切って使う。

#Preview {
    StarRating(rating: .constant(3))
}

.constant() で作ったバインディングは書き込みが無視されるため、インタラクションの確認には向かない。動作テストには親ビューごとプレビューするか、@Previewable @State を使うとよいだろう。

@Binding は SwiftUI のデータフローにおいて「所有しないが操作する」という関係を表現する重要な道具だ。@State が「持つ」、@Binding が「借りる」という対の関係を意識すると、ビュー間のデータの流れが明確になる。