【Swift/Observation】@Observableマクロの使い方!状態監視の仕組み

【Swift/Observation】@Observableマクロの使い方!状態監視の仕組み

この記事からわかること

  • Swift@Observableとは?
  • Observationフレームワークとは?
  • 状態管理実装方法
  • @BindablewithObservationTracking使い方

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

Observationフレームワークとは?

公式リファレンス:Observation

ObservationフレームワークはSwift5.9 / iOS17から導入されたデータの変更を監視(observe)して変化を通知する新しい仕組みです。これまでデータの監視にはCombineベースの「ObservableObject + @Published」が活用されていましたが、これに置き換わる方法としてAppleも公式に推奨しています。

公式リファレンス:Migrating from the Observable Object protocol to the Observable macro

ObservationフレームワークではCombineから脱却しSwiftの言語機能(マクロ)で自動的に監視コードを生成することができるようになります。そしてObservationフレームワークで肝となるマクロが@Observableになります。Swift UIベースの開発で使いやすいように定義されていますがUIKitベースでも問題なく動作させることが可能です。

@Observableの使い方とObservableObjectとの違い

@Observableマクロの使い方を見る上で従来のObservableObjectプロトコルとの実装の違いも見ていきたいと思います。簡単なカウンターアプリを想定しています。

ObservableObject + @Published

まずは従来の方法だと以下のような実装になります。ViewModel側にはObservableObjectの準拠と値の変更監視対象を明示的に@Publishedを使用して宣言します。View側では@StateObjectなどを使用して変更を監視し、変化時に再描画が走るようにしておきます。

class CounterViewModel: ObservableObject { // ObservableObjectを継承
    // 値の変更を監視対象にする
    @Published  var count = 0
    func increment() { count += 1 }
}

struct ContentView: View {
    // ObservableObjectを監視する
    @StateObject  private var viewModel = CounterViewModel()
    
    var body: some View {
        VStack {
            Text("Count: \(viewModel.count)")
            Button {
                viewModel.increment()
            } label: {
                Text("increment")
            }
        }
    }
}

@Observableマクロ

@Observableマクロの使い方は対象のViewModelに@Observableを付与するだけです。これにより対象のクラスのもつプロパティが全て自動で監視対象になります。View側では今回変更可能にしているので@Stateを付与していますが、@Stateの付与がなくても変更が走れば再描画されるようになっています。

@Observable
class CounterViewModel {
    var count = 0
    func increment() { count += 1 }
}

struct ContentView: View {
    // View内で変更しているので@Stateを付与
    @State  private var viewModel = CounterViewModel()
    
    var body: some View {
        VStack {
            Text("Count: \(viewModel.count)")
            Button {
                viewModel.increment()
            } label: {
                Text("increment")
            }
        }
    }
}

@Observableの特徴

公式リファレンス:Managing model data in your app

特徴まとめ

構造体には使えない

@Observable構造体(struct)では使用できずクラス(class)のみで使用できます。これはObservationフレームワークが参照型のオブジェクトを対象としているからです。値型の構造体では都度インスタンスのコピーが作られる仕組みのため変更の監視を行うことができないようです。

@Observable  // ✖︎
struct Book: Identifiable {
    var title = "Sample Book Title"
    var author = Author()
    var isAvailable = true
}

computed property(計算プロパティ)も監視対象

@Observableを付与したクラスではcomputed property(計算プロパティ)も監視対象になります。例えばavailableBooksCount(コレクションの特定の条件にマッチするカウント)を計算するプロパティを用意しておいた場合に、任意のコレクションの要素のisAvailableに変化が起きた場合にViewに対して変化を通知することが可能です。

@Observable  class Library {
    var books: [Book] = [Book(), Book(), Book()]
    
    var availableBooksCount: Int {
        books.filter(\.isAvailable).count
    }
}

struct LibraryView: View {
    @Environment(Library.self) private var library
    
    var body: some View {
        NavigationStack {
            List(library.books) { book in
                // ...
            }
            .navigationTitle("Books available: \(library.availableBooksCount)")
        }
    }
}

再描画の依存関係

Apple公式では@Observableを使ったサンプルコードとしてモデル自体を監視可能にしています。例えば以下のように複数の監視対象となるプロパティを持つモデルクラスがあるとします。

@Observable
final class Book: Identifiable {
    var title = "Sample Book Title"
    var author = Author()
    var isAvailable = true
}

これをView内で使用する際には以下のようになりますが、この際にView内で読み取っているプロパティの変化のみが再描画のトリガーになります。つまり今回で言うとauthorisAvailableに変化が起きてもBookView自体は再描画されません。

struct BookView: View {
    var book: Book
    
    var body: some View {
        Text(book.title)
    }
}

またコレクション型で保持している場合はコレクションに対する要素の追加・削除も変更監視対象となり期待通りに再描画されるようになります。またここで嬉しいのは1つの要素(Book)のプロパティが更新された際にList全体ではなく、対象のBookを参照しているViewのみが再描画されるように最適化されています。

struct LibraryView: View {
    @State  private var books = [Book(), Book(), Book()]

    var body: some View {
        List(books) { book in 
            Text(book.title)
        }
    }
}

@Bindable

公式リファレンス:@Bindable

@Bindableを使用すると双方向バインディングを実現することが可能です。View側での更新をリアルタイムでBookモデル側にも反映させることができるようになります。

struct BookEditView: View {
    @Bindable  var book: Book
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        Form {
            TextField("Title", text: $book.title)
            Toggle("Book is available", isOn: $book.isAvailable)
            Button("Close") {
                dismiss()
            }
        }
    }
}

withObservationTracking

ViewではなくView以外の部分やUIKit側からObservationフレームワークを活用するにはwithObservationTrackingを使用します。withObservationTrackingは低レベル層のAPIで@Observableなどで内部的に自動でやってくれていた部分を手動で実装できる役割を持っています。


public func withObservationTracking<T>(_ apply: () -> T, onChange: @autoclosure () -> @Sendable () -> Void) -> T

applyでは監視対象を参照するように実装し、onChange変化が起きた際に実行したい処理を実装します。言葉では理解しづらいのでコードで確認してみます。

@Observable
class Counter {
    var count = 0
}

let counter = Counter()

// 観測をセットアップ
withObservationTracking {
    // counter.countを監視対象にしたいため参照を持たせる
    print("現在のカウント:\(counter.count)")
} onChange: {
    // 変更が起きた際に実行したい処理
    print("カウントが変更: \(counter.count)")
}

上記のように1つ目のクロージャーの中ではただ値を参照するだけ、2つ目のクロージャーの中で変化した際に発火させたい処理を記述します。またwithObservationTrackingでの監視は1回だけなので2回目の変更があっても期待通りに動作しません。

そのため永続的に監視を続けたい場合はonChangeの中で再起的にwithObservationTrackingを呼び出すような構造で実装してあげる必要があります。

private func tracking() {
    withObservationTracking {
      // 〜〜〜〜
    } onChange: {
        tracking()
    }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

Search Box

Sponsor

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index