[SwiftUI][Observation] Bindable とは何か?

SwiftUI2021

     
⌛️ 3 min.

SwiftUI の要素で、macOS14/ iOS17 で登場した Bindable について いろいろと確認してみます。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS15.1.1 Sequoia
  • Xcode 16.2
  • iOS 18.2RC
  • Swift 5.9

Bindable

@マークで始まる attribute は、macro だったりといろいろとありますが、Bindable は property wrapper です。


参考
BindableApple Developer Documentation

どういう時に使用する?

1文で表現すると以下になります。

「Observable 指定されたオブジェクトの プロパティを Binding として渡すときの前準備としてクラスに対して指定する」

注意

Bindable と Binding は別のものです。

背景含め 順番に説明していきます。

以下を前提知識として 説明していきます。

  • @StateObject と @Published を使用して reference 型のデータを使ったことがある/使える
  • Binding を必要とする SwiftUI の コントロールを使ったことがある/使える

上記に必要性を感じたことがなければ、Bindable は 必要ない気がします。(個人の感想です)

Observable 指定されたオブジェクトの プロパティ

macro Observable を付与できる対象は、class だけです。

MEMO

Apple のドキュメントに明確には書かれていませんが、「class 以外に付与してもエラーになる」、「動作から考えると class 以外を Observable にするのは難しい」ということで、上記の理解をしています。

そして、Observable を付与するのは、その class のプロパティの変更検知をしたいときです。

[Swift][Observation] Observation の仕組み その1: Observable

@StateObject 時代の ObservableObject/@Published

@StateObject 時代(?)は、変更検知したい class オブジェクトは ObservableObject に conform させ、変更を検知したいプロパティには、@Published を付与していました。

こうすることで、該当プロパティが変更されたときには、ObservableObject の objectWillChange が send されていました。

ですが、@StateObject/@Published は、ネストしたオブジェクトの変更検知が行われないという不便な点がありました。
また 事前にクラスを ObservableObject を conform させ、監視対象プロパティに @Published を付与しておくことが必要でした。

Observable 指定されたオブジェクトのプロパティ

Observable 指定されたオブジェクトの プロパティは、macro Observable の仕組みで 変更検知できます。

@Observable では、オブジェクトに @Observable を付与することは必要ですが、監視対象プロパティについては特に指定する必要がありません。逆に監視対象から “外したい” ときに @ObservationIgnored を付与することが必要でした。

ただ、困るのは TextField や Picker のような “Binding” を要求してくる View です。

@Observable
class Book: Identifiable {
    var title: String
    init(title: String) {
        self.title = title
    }
}
struct BookEditView: View {
    @State private var text = "Hello world"

    var book: Book = Book(title: "Hello Observable")

    var body: some View {
        Form {
            TextField("Text", text: $text)               // もちろん OK
            TextField("BookTitle", text: $book.title)    // error: Cannot find '$book' in scope
        }
    }
}

試しに(?)、@ObservableObject と同様の $ をつけてみても動作しません。

ObservedObject 指定された class の @Published 指定されたプロパティは、value-type のプロパティと 同じように $ マークを付与することで Binding として渡すことが可能でした。
ですが、これは ObservedObject/ @Published を付与していたことで得られていた特性であり、@Observable 指定された class のプロパティにそのまま $ を付与して Binding にすることはできません。

MEMO

TextField のような変数に変更を加えるための View は、Reference-type な class だけでなく、value-type な Struct や enum のデータについても処理することが必要であるため、Binding が必要です。

ちなみに、@Observable 指定された class のプロパティに @Published 指定することはできません。(実装が競合してしまうので、エラーとなります。)

@Observable
class Book: Identifiable {
    var title: String
    @Published var isbn: String    // error: '_isbn' synthesized for property wrapper backing storage
    var bought: Bool
    init(title: String, isbn: String, bought: Bool) {
        self.title = title
        self.isbn = isbn
        self.bought = bought
    }
}

ということで、このような Binding を要求する View に Obervable なオブジェクトのプロパティを “Binding” として渡すための仕組みが必要となりました。

そのための準備(?) が “Bindable” です。

@Bindable 指定すると、上記の $ 指定が使えるようになります。

使用例

文章だけで説明しても、わかりにくい気がするので、コードを見ていきます。

以下の Book は、Apple のドキュメントのサンプルを真似て作ってみました。

@Observable
class Book: Identifiable {
    var title: String
    var isbn: String
    var isBought: Bool
    init(title: String, isbn: String, isBought: Bool) {
        self.title = title
        self.isbn = isbn
        self.isBought = isBought
    }
}

Observable を表示してみる

まずは、表示してみます。

@Observable 指定されているとしても特別な配慮は不要です。

import SwiftUI
import Observation

struct ContentView: View {
    @State private var books = [Book(title: "Book1", isbn: "978-X-XX-XXXXX1-X", isBought: true),
                                Book(title: "Book2", isbn: "978-X-XX-XXXXX2-X", isBought: false),
                                Book(title: "Book3", isbn: "978-X-XX-XXXXX3-X", isBought: false),
                                                            ]
    var body: some View {
        VStack {
            Text("BookList").font(.title)
            List(books) { book in
                BookListRow(book: book)
            }
        }
        .padding()
    }
}

struct BookListRow: View {
    let book: Book
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Text(book.title).font(.largeTitle)
            HStack {
                Text(book.isbn)
                Spacer()
                Text("Bought").padding(3).strikethrough(!book.isBought, pattern: .solid, color: .red)
                    .background(RoundedRectangle(cornerRadius: 5).fill(book.isBought ? .green : .yellow))
            }.font(.title)
        }
    }
}

普通に プロパティを参照して 表示できます。

BookList

MEMO

なお、@State は、導入当初は value-type な変数に付与する使い方でしたが、Observation の導入に合わせて拡張されました。value-type だけでなくreference-type にも付与できるようになっています。

Binding を渡す例

NavigationStackView を使用して、編集画面をつけて、Book を変更できるようにしてみます。

import SwiftUI
import Observation

struct ContentView: View {
    @State private var books = [Book(title: "Book1", isbn: "978-X-XX-XXXXX1-X", isBought: true),
                                Book(title: "Book2", isbn: "978-X-XX-XXXXX2-X", isBought: false),
                                Book(title: "Book3", isbn: "978-X-XX-XXXXX3-X", isBought: false)
                                ]

    @State private var selection: Book.ID?

    var body: some View {
        NavigationSplitView(sidebar: {
            VStack {
                Text("BookList").font(.title)
                List(books, selection: $selection, rowContent: { book in
                    BookListRow(book: book)
                })
                .listStyle(.sidebar)
            }
        }, detail: {
            if let selection = selection,
               let book = books.first(where: { $0.id == selection }) {
                BookDetailForm(book: book)
            } else {
                Text("select from sidebar")
            }
        })
        .padding()
    }
}

struct BookListRow: View {
    let book: Book
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Text(book.title).font(.largeTitle)
            HStack {
                Text(book.isbn)
                Spacer()
                Text("Bought").padding(3).strikethrough(!book.isBought, pattern: .solid, color: .red)
                    .background(RoundedRectangle(cornerRadius: 5).fill(book.isBought ? .green : .yellow))
            }.font(.title)
        }
    }
}

struct BookDetailForm: View {
    @Bindable var book: Book         // !!! @Bindable 指定 !!!
    var body: some View {
        Form {
            TextField("Title", text: $book.title)
            TextField("ISBN", text: $book.isbn)
            Toggle(isOn: $book.isBought, label: { Text("Bought?") })
        }.padding()
    }
}

上記の BookDetailForm で、受け取っている Book を @Bindable 指定しています。
これは、View 内部で Binding を必要とする TextField や Toggle を使用して Book のプロパティを変更したいからです。

表示するだけの BookListRow では、@Bindable が不要であることと対象的です。

BindableBookList

この BookDetailForm での使用シーンが、 @Bindable が使用されるシーンのすべてです。

まとめ

@Bindable とは何かを まとめてみました。

@Bindable とは何か
  • Observation Framework と合わせて導入されました
  • @Observable 指定したオブジェクトに付与して使用します
  • @Bindable を付与したオブジェクトのプロパティの Binding を提供します

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です