【SwiftUI】@Bindingと@Bindableの違い

# この記事は何？

SwiftUIの`@Binding`と`@Bindable`、これら2つのプロパティラッパーについて理解を深めるための解説。

> SwiftUIのプロパティラッパーに関しては、[こちらの記事](https://qiita.com/imchino/items/422140d55f9168a5933d)で基本的な部分を解説済み。

> Swiftを基礎から学ぶには
自著、工学社より発売中の「[まるごと分かるSwiftプログラミング](https://amzn.asia/d/7YPWJwl)」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
> また、Swiftプログラミングを基礎から動画で学びたい方には、Udemyコース「[今日からはじめるプログラミング](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwii1qTuj_2BAxV4hlYBHbj5BqEQFnoECBYQAQ&url=https%3A%2F%2Fwww.udemy.com%2Fcourse%2Fstart-programming-today%2F&usg=AOvVaw2vs4Sccq-bpfA4ZD2rCyNm&opi=89978449)」をお勧めします。

## BindingとBindable

どちらもSwiftUIフレームワークのプロパティラッパーだが、新しい`@Bindable`はiOS17以降で利用可能。
これらのプロパティラッパーは用途も似ていて、主に、テキストフィールドのようなコントロール系ビューが受け取るためのバインディングを作成するために利用される。

`@Bindable`プロパティラッパーは[その開発者向けドキュメント](https://developer.apple.com/documentation/swiftui/bindable)では、「*A property wrapper type that supports creating bindings to the mutable properties of observable objects.*」と説明されている。
要するに、「`Observable`なオブジェクトの変数プロパティへのバインディングを作成する」ということ。

一方、`@Binding`プロパティラッパーは[その開発者向けドキュメント](https://developer.apple.com/documentation/swiftui/binding)で、「*A property wrapper type that can read and write a value owned by a source of truth.*」と説明されている。
これは「信頼できる情報源が所有する値を読み書きできる」ということ。

# Bindingプロパティラッパーの使いどころ

`@Binding`を理解するためには、`@State`プロパティラッパーの理解が前提となる。
`@State`がマークされた変数は「ビューのステート（状態）」であり、SwiftUIによって「その値の状態」が追跡される。
`@State`変数の値が変更されると、SwiftUIはそのビューを更新して、画面の最新状態を表示する。

![‎State and Binding Basics.‎001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/a0a4cf47-84b9-1eb8-4266-bba5e057dbf1.png)

この`isPlaying`変数については、`body`プロパティの外側で宣言されている点にも注目。
これは、「ビューとステート（状態）の分離」を意味している。

## 音楽プレイヤー画面の再生ボタン

以下の例では、`MusicPlayer`ビューは、音楽プレイヤーの画面を作成する。
これは、テキストとボタンだけで構成される。
また、`PlayButton`ビューはボタン部分だけを抽象化した構造体。
ボタンをタップするたびに、プレイヤー画面の状態は「一時停止」と「再生」を交互に変化する。

まず、`isPlay`変数にマークされた`@Binding`に注目。
この`isPlay`変数は既定値がないので、`PlayButton`ビューの初期化の際に`Bool`型の既定値を設定する。
ただし、`@Binding`な変数を初期化するために設定できる既定値は通常の値ではなく、「SwiftUIが追跡する`@State`変数への参照」を設定する。

`@Binding`な変数を初期化するために「SwiftUIが追跡する`@State`変数への参照」を設定するには、`@State`な変数の先頭に`$`記号を記述する。

例えば、`PlayButton()`イニシャライザの`isPlay`パラメータに設定するのは通常の`Bool`値ではなく、`Binding<Bool>`型の**バインド値**を渡す。
`PlayButton`ビューの`@Binding`変数を初期化する際に「SwiftUIが追跡する`@State`変数への参照」を設定している。

```swift:ボタンを抽象化したコード
import SwiftUI

// 再生ボタンのビュー（子）
struct PlayButton: View {
    @Binding var isPlay: Bool    // Bool型のバインドを受け取る
    
    var body: some View {
        Button {
            isPlay.toggle()
        } label: {
            Image(systemName: isPlay ? "pause.circle" : "play.circle")
        } 
    }
}

// 音楽プレイヤーのビュー（親）
struct MusicPlayer: View {
    @State var isPlaying = false    // SwiftUIが「値の変更を」追跡する状態プロパティ
    
    var body: some View {
        VStack {
            Text("Awesome music")
                .foregroundStyle(isPlaying ? .gray : .green)
            PlayButton(isPlay: $isPlaying)    // $記号で作成したバインドを渡す
        }        
    }
}
```

`PlayButton`ビューを初期化する際に「`isPlaying`プロパティのバインドを渡している。
つまり、これは「`PlayButton`ビューが`isPlaying`プロパティの値を書き換える」ことを意味する。

![‎State and Binding Basics.‎002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/94d0e7ef-dc35-977b-33bf-7fcb602c7271.png)

# Bindableプロパティラッパーの使いどころ

「観測可能なオブジェクトにある変数プロパティへの参照」のバインディングを作成するプロパティラッパー型。
`@Bindable`プロパティラッパーを使用すると、`Observable`プロトコルに準拠したデータモデルオブジェクトの変数プロパティへのバインディングを作成できる。

## 単純なケース

以下の例において、データモデルの`Book`クラスは「信頼できる情報源」。
そして、`BookEditView`ビューは変数`book`に`@Bindable`をマークしている。
その`body`では、`TextField`ビューが`book`のバインドを利用して、`title`プロパティを変更する。
また、`Toggle`ビューは`book`の`isAvailable`プロパティを変更する。
ここでは、各プロパティのバインディングを渡すために、どちらのコントロールビューに対しても`$`構文を使用している点に注目。

```swift:
// 観測可能なオブジェクトのデータモデル
@Observable
class Book: Identifiable {
    var title = "Sample Book Title"
    var isAvailable = true
}

// 図書の情報を編集するためのビュー
struct BookEditView: View {
    @Bindable var book: Book    // bookは信頼できる情報源にバインド
    @Environment(\.dismiss) private var dismiss
        
    // TextFieldとToggleは「bookへのバインド」を受け取る
    var body: some View {
        Form {
            TextField("Title", text: $book.title)
            Toggle("Book is available", isOn: $book.isAvailable)
            Button("Close") {
                dismiss()
            }
        }
    }
}
```

上のコードにおいて、ビューのプロパティとして作成されるバインド変数には、既定値を設定しない点に注意。
実際のインスタンスは、親ビューから渡される。

## ケース2

`Bindable`プロパティラッパーは、`Observable`オブジェクトの変数およびプロパティにも使用できる。
これにはグローバル変数や「SwiftUIの外部にある型のプロパティ」だけでなく、ローカル変数が含まれる。

例えば、以下の`LibraryView`ビューは**body内で`@Bindable`変数を作成**する。
ボディのリスト内でバインド変数を作成することによって、それぞれの要素を参照するバインドを取得できる。

```swift:
struct LibraryView: View {
    // Book型インスタンスはObservableなデータモデル
    @State private var books = [Book(), Book(), Book()]

    var body: some View {
        List(books) { book in
            @Bindable var book = book
            TextField("Title", text: $book.title)
        }
    }
}
```

ここではまた、`@Bindable`な変数に**既定値を設定している**点に注目できる（最初のケースでは既定値を設定していない）。
`@Bindable`な変数`book`は「`book`データモデルに接続するバインディング」を提供するので、`TextField`ビューは「モデルデータの`title`プロパティ」にアクセスして、変更を加えることができる。

## ケース3

ケース2と同じ手法を、「観測可能なオブジェクトへのバインディング」がビューの環境に格納されている場合も使用できる。

例えば、次のコードは`@Environment()`プロパティラッパーを使用して、環境にある`Observable`な`Book`型インスタンスを取得する。
そして、ビューの`body`内で`@Bindable`な変数`book`を作成して、`$`記号をマークして「`book`データモデルの`title`プロパティまでのバインディング」を`TextField`ビューに渡す。

```swift:
struct TitleEditView: View {
    // bookプロパティに「ビューの環境にあるモデルデータ」を取得
    @Environment(Book.self) private var book

    var body: some View {
        @Bindable var book = book    // 環境にあるモデルデータの参照をバインド
        TextField("Title", text: $book.title)
    }
}
```

ここでもやはり、バインド変数に既定値を設定する必要がある。