18
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Swift5.9からのObservationはどのような仕組みでクラスのプロパティを監視しているのか

Last updated at Posted at 2024-02-09

はじめに

Swift 5.9からのObservationがどうやってプロパティを監視しているのか、Swiftのコードから仕組みについて書いておきます。Swiftのコードを読み間違えて解釈していることもあるかもしれませんが、誤りなど見つけたらコメントをください。

本題

withObservationTrackingについて

Observationのプロポーザルから、次のようなコードを見たことがあると思います。これはクラスCarのnameとnumberの変更を監視し、.onChangeが動作するというものです。

import Observation

@Observable
class Car {
  let name: String
  let number: String
}

let cars: [Car] = ...

@MainActor
func renderCars() {
    withObservationTracking {
        for car in cars {
            print(car.name)
        }
    } onChange: {
        Task { @MainActor in
            renderCars()
        }
    }
}

最初見た時不思議さを感じるがSwiftUI.Viewのbodyに使うことを考慮した自由さだろう

最初これ見た時に私が不思議だなと感じたのは、このwithObservationTrackingの1つ目のクロージャがprintをしている意味のわからなさです。これprintじゃなくてもいいんですが、監視されるのはprintされているnameだけではなくnumberも監視しますので、実質ここではcarが呼び出されていればいいだけなのです。

しかしそれであれば、クロージャでオブジェクトの配列を渡せばいいだけなのではと思いませんか?withObservationTracking(cars).onChange{}でいいじゃんって気がします。
まあ多分それもやろうと思えばできるんです。ただ、このクロージャだからこそある程度の自由さがあるのでしょう。これは主にSwiftUI.bodyのような処理の場合に便利だからまあそうなってると納得できなくもないです。

最初に結論、どうなってるのwithObservationTracking

withObservationTrackingの内部の実装では、1つ目のクロージャに記載されたオブジェクトからAccessListを取り出し一つにマージしてそのAccessListに変化が通知されればonChangeを実行するようになっています。

例えばcar.nameと書かれたらcarのAccessListを取得しているわけです。だからcar.numberの変化も取得できる、くらいの理解でいいはずです。

間にObservationRegisterなんかもありますが、これはSwift Macroによって生成されるものです。だいたい図にすると次のようになります。

スクリーンショット 2024-02-09 17.28.08.png

以下はこれをちょっとだけ詳しく書いておきます。

@ObservableのSwift Macro

変化を監視される側がどのように変化を通知するのかをまず掘り下げましょう。

さきほどのクラスCarにつけた@ObservableについてSwift MacroをXcode上で展開するとnamenumberについて@ObservationTrackedが付加されます。name@ObservationTrackedも展開してみると次のようなコードが生成されているのがわかるはずです。

@Observable
class Car {
+   @ObservationTracked
    var name: String
+   {
+        @storageRestrictions(initializes: _name)
+        init(initialValue) {
+           _name = initialValue
+        }
+
+        get {
+           access(keyPath: \.name)
+           return _name
+        }
+        set {
+           withMutation(keyPath: \.name) {
+           _name = newValue
+        }
+    }
+    @ObservationIgnored private var _name: String

+    @ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar()
+
+    internal nonisolated func access<Member>(
+        keyPath: KeyPath<Car , Member>
+    ) {
+        _$observationRegistrar.access(self, keyPath: keyPath)
+    }
+
+    internal nonisolated func withMutation<Member, MutationResult>(
+        keyPath: KeyPath<Car , Member>,
+        _ mutation: () throws -> MutationResult
+    ) rethrows -> MutationResult {
+        try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
+    }

     @ObservationTracked
     var number: String
}

生成されたコードについては下記の通りです。

  • @ObservationIgnored private var _name: String
    • 値の実体が生成された
  • @ObservationTracked var name: String { ... }
    • ストアドプロパティがコンピューティッドプロパティにされた
      • get
        • 下記のfunc accessを呼び出す
        • _nameの値を返す
      • set
        • 下記のfunc withMutationを呼び出す
        • _nameの値を変更
  • @ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar()
  • func access
    • observationRegistrarのaccessを呼び出す
  • func withMutation
    • observationRegistrarのwithMutationを呼び出す

observationRegistrarのaccess/withMutationはそれぞれがAccessListに対しての操作となり、accessはAccessListがなければ作成し登録。withMutationはwillSet/didSet時にAccessListに対して変更を通知する機能を持ちます。

つまり、Swift MacroによってそのオブジェクトのAccessListが作られ、get時に登録され、set時に変更をAccessListにその変更を通知するようになったというわけです。

SwiftUIの場合

SwiftUIでObservationを使う場合、withObservationTrackingを書きません。

予想ですが、これは我々が見えないところでこのvar bodyに対して繰り替しwithObservationTrackingのような処理を呼び出すような仕組みが働いているんでしょう。

struct SmoothieList: View {
    var car: Car
        
    var body: some View {
        // ここでwithObservationTrackingの繰り返しを使っていない。
        // 上位のレベルでシステムがAccessLisetから繰り返し変更通知を受け取れるようにしているんでしょう。
        Text(car.name)
    }
} 

Observationの仕組みをそのまま利用したバックポートが作れるのでは?

Observationの仕組みが理解できたところで、そのままそのコードを利用してiOS17未満のためのバックポートが作れるのではないかと考える人もいるでしょう。

これはswift-perceptionとしてPointFreeの2人がOSSとして公開しています。

まとめ

もう一度最初の図から振り返ってみます

スクリーンショット 2024-02-09 17.28.08.png

  • 参照型のオブジェクトはSwift MacroによってOvservationRegisterを持ちます
  • Swift Macroによって対象のプロパティはget/setが用意されます
  • get/setによってAccessListの登録/AccessListへの変更通知がされるようになります
  • withObservationTracking(:)は複数のAccessListを取り出し、それをマージして変更を検知します
  • 変更を検知するとonChangeが動作します

参考

実際のapple/swiftの実装

withObservationTracking(:)

AccessListのマージ

発表資料

18
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?