この記事は何?
SwiftUIの@Binding
と@Bindable
、これら2つのプロパティラッパーについて理解を深めるための解説。
SwiftUIのプロパティラッパーに関しては、こちらの記事で基本的な部分を解説済み。
Swiftを基礎から学ぶには
自著、工学社より発売中の「まるごと分かるSwiftプログラミング」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
また、Swiftプログラミングを基礎から動画で学びたい方には、Udemyコース「今日からはじめるプログラミング」をお勧めします。
BindingとBindable
どちらもSwiftUIフレームワークのプロパティラッパーだが、新しい@Bindable
はiOS17以降で利用可能。
これらのプロパティラッパーは用途も似ていて、主に、テキストフィールドのようなコントロール系ビューが受け取るためのバインディングを作成するために利用される。
@Bindable
プロパティラッパーはその開発者向けドキュメントでは、「A property wrapper type that supports creating bindings to the mutable properties of observable objects.」と説明されている。
要するに、「Observable
なオブジェクトの変数プロパティへのバインディングを作成する」ということ。
一方、@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はそのビューを更新して、画面の最新状態を表示する。
この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
変数への参照」を設定している。
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
プロパティの値を書き換える」ことを意味する。
Bindableプロパティラッパーの使いどころ
「観測可能なオブジェクトにある変数プロパティへの参照」のバインディングを作成するプロパティラッパー型。
@Bindable
プロパティラッパーを使用すると、Observable
プロトコルに準拠したデータモデルオブジェクトの変数プロパティへのバインディングを作成できる。
単純なケース
以下の例において、データモデルのBook
クラスは「信頼できる情報源」。
そして、BookEditView
ビューは変数book
に@Bindable
をマークしている。
そのbody
では、TextField
ビューがbook
のバインドを利用して、title
プロパティを変更する。
また、Toggle
ビューはbook
のisAvailable
プロパティを変更する。
ここでは、各プロパティのバインディングを渡すために、どちらのコントロールビューに対しても$
構文を使用している点に注目。
// 観測可能なオブジェクトのデータモデル
@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
変数を作成する。
ボディのリスト内でバインド変数を作成することによって、それぞれの要素を参照するバインドを取得できる。
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
ビューに渡す。
struct TitleEditView: View {
// bookプロパティに「ビューの環境にあるモデルデータ」を取得
@Environment(Book.self) private var book
var body: some View {
@Bindable var book = book // 環境にあるモデルデータの参照をバインド
TextField("Title", text: $book.title)
}
}
ここでもやはり、バインド変数に既定値を設定する必要がある。