[Swift][Observation] Observation の仕組み その2: withObservationTracking

     
⌛️ 2 min.

iOS17/ macOS14 で導入された Observation の仕組みを見ていきます。その2: withObservationTracking

環境&対象

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

  • macOS15.0.1 Sequoia
  • Xcode 16.1 Beta
  • iOS 18.1
  • Swift 6

Observation 解説シリーズ記事

Observation を説明してます。

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

withObservationTracking

withObservationTracking は、Observation フレームワークの一部として導入されました。


参考
withObservationTracking(_:onChange:)Apple Developer Documentation

1つの関数です。この関数を使うことで、使用されるプロパティとその変更通知を関連づけていくことになります。

Observable マクロを使うことで、(Observable な)オブジェクトについてアクセス/ 変更管理 できるようにしました。
具体的には、 ObservationRegistrar を使用していました。

動作を見ていかないとわかりにくいので、例によって UnitTest を使って試していきます。

UnitTest での確認

動作を確認するのにアプリを作るのは大変なので、UnitTest で確認していくことにします。

プロジェクトのターゲットに 通常(?) の UnitTest を追加します。

(ユニットテストのフォルダ下に) コードとして以下のコードを追加して試していきます。

import Testing
import Observation

struct WithObservationTrackTest {

    @Test func simpleUse() async throws {
        // code...
    }

}

上記の struct に追加でテストするメソッドを追加して動作を確認していきます。

シンプルな使用例

まずは、Observable のドキュメントにもあるような使い方をしてみます。

対象は、Observable な class ですので、以下のような class 定義をします。

@Observable
class Car {
    var name: String = "DeLorean"
    var engineSize: Int = 2849
}

@Observable を付与しているので、Car は、Observable に conform します。

@ObservationIgnored は付与されていないので、プロパティ name, engineSize いずれも Observation の対象となります。

ドキュメントにあるようなコードで動作を見てみます。

@Observable
class Car {
    var name: String = "DeLorean"
    var engineSize: Int = 2849
}

struct WithObservationTrackTest {

    @Test func simpleUse() async throws {
        let car = Car()

        await confirmation("nameChangeDetect", { confirm in
            withObservationTracking({
                let _ = car.name
            }, onChange: {
                confirm.confirm()
            })
            car.name = "Ford"
        })
    }
}

上記は、car.name を変更した時に、withObservationTracking の onChange が呼ばれていることを確認するテストコードです。

withObservationTracking の apply (最初の closure) でアクセスしている name が変更されると onChange が呼ばれることをテストしています。。

withObservationTracking の apply (最初の closure) でアクセスしていない engineSize が変更されても onChange が呼ばれないこともテストしてみます。

    @Test func simpleUseNoNotify() async throws {
        let car = Car()

        await confirmation("sizeChangeNoDetect", expectedCount: 0, { confirm in
            withObservationTracking({
                let _ = car.name
            }, onChange: {
                confirm.confirm()
            })
            car.engineSize = 1000
        })

    }

このテストも、パスしますので、ドキュメンテーション通りの動きであることが確認できます。(当然ですが・・)

onChange の呼ばれるタイミング

onChange の呼ばれるタイミングを確認してみます。

@Observable
class Car {
    var name: String = "DeLorean" {
        willSet { print("willSet") }
        didSet { print("didSet") }
    }
    var engineSize: Int = 2849
}

struct WithObservationTrackTest {

    @Test func simpleUse() async throws {
        let car = Car()

        await confirmation("nameChangeDetect", { confirm in
            withObservationTracking({
                let _ = car.name
            }, onChange: {
                print("confirm")
                confirm.confirm()
            })
            car.name = "Ford"
        })
    }
}

実行してみると以下のようなログになりました。

􀟈 Suite WithObservationTrackTest started.
􀟈 Test simpleUse() started.
confirm
willSet
didSet
􁁛 Test simpleUse() passed after 0.001 seconds.
􁁛 Suite WithObservationTrackTest passed after 0.001 seconds.
􁁛 Test run with 1 test passed after 0.001 seconds.

willSet よりも前に onChange は、呼び出されているようです。

SwiftUI との関係性

Observable は多くのケースで SwiftUI の描画更新に使用されている気がします。(実質、SwiftUI 向けでしょう)

SwiftUI との関係性も少し考察してみます。

ポイントは、以下な気がします。
・withObservationTracking の apply(最初の closure) の返り値が withObservationTracking の返り値になる
・onChange が呼ばれるのは、変更後の1回のみ
・onChange は、willSet 前に呼ばれる

SwiftUI でどのように使用されているのか、SwiftUI 側を”想像” してみます。

SwiftUI は、画面描画を行う時に、直接 body を使用するのではなく renderingView を経由するものと想像してください。

struct SomeView: View {
    var body: some View {
      // ... 省略
    }

    var renderingView: any View {
        withObservationTracking({
            self.body
        }, onChange: {
            // refresh view
        })
    }

withObservationTracking を通して body が評価されるので、参照しているプロパティについての Tracking を開始します。

参照しているプロパティが更新されると onChange が呼ばれます。このとき、SwiftUI に 自 View を再描画する設定を行います。

この時点で、Tracking は解除されます。何らかの理由で 再描画が行われないケースでは、body が再評価されないので Tracking は行われず、以降 通知されることもなくなります。

再度 描画を行う必要があれば、withObservationTracking の apply で body が再度評価されるので、必要なプロパティについての Tracking を再度行うことになります。

以降は、更新があれば onChange が呼ばれ・・・ というサイクルを繰り返すことが可能です。

この使用方法を考えると、onChange が “1度だけ呼ばれる” という仕様も 理解できます。

onChange が willSet の前のタイミングで呼ばれることも、アニメーション等が必要な SwiftUI 向けに設計されていることを窺わせます。

SwiftUI 以外との関係性

公開されている Observation@githubを見てみると、withObservationTracking には、onChange 以外にも willSet や didSet が用意されていることがわかります。

ですが、ほとんどの関数に_spi 指定されていて、公開されているのは、onChange だけのようです。

onChange 以外が公開されていない理由は分かりませんが、公開されると SwiftUI 以外での使い道も見つかる気がします。

# UI 更新には willSet が便利ですが、データの連携等は didSet の方が便利な気がします。

まとめ

withObservationTracking の仕組みを見てみました。

withObservationTracking の仕組み
  • withObservationTracking は、アクセスを記録し、変更を検知すると onChange を呼び出す
  • onChange の呼び出しは、一度だけ
  • onChange は、willSet の前に呼び出される

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

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版が最新版です。

コメントを残す

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