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

# はじめに

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

# 本題

## withObservationTrackingについて

Observationの[プロポーザル](https://github.com/apple/swift-evolution/blob/main/proposals/0395-observability.md)から、次のようなコードを見たことがあると思います。これはクラスCarのnameとnumberの変更を監視し、.onChangeが動作するというものです。


```swift
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16400/15d8e8ac-0bfc-ab39-4df5-4be3994517d6.png)


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

## `@Observable`のSwift Macro

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

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


```swift
@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`のような処理を呼び出すような仕組みが働いているんでしょう。

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

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

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

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

https://github.com/pointfreeco/swift-perception


# まとめ

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

![スクリーンショット 2024-02-09 17.28.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16400/15d8e8ac-0bfc-ab39-4df5-4be3994517d6.png)

- 参照型のオブジェクトはSwift MacroによってOvservationRegisterを持ちます
- Swift Macroによって対象のプロパティはget/setが用意されます
- get/setによってAccessListの登録/AccessListへの変更通知がされるようになります
- `withObservationTracking（:)`は複数のAccessListを取り出し、それをマージして変更を検知します
- 変更を検知するとonChangeが動作します
    - [メソッドによってはonChangeがwillSetやdidSetのものもあります](https://github.com/apple/swift/blob/swift-5.9-RELEASE/stdlib/public/Observation/Sources/Observation/ObservationTracking.swift#L190-L222)

# 参考

## 実際のapple/swiftの実装

withObservationTracking（:)

https://github.com/apple/swift/blob/swift-5.9-RELEASE/stdlib/public/Observation/Sources/Observation/ObservationTracking.swift#L179-L188

AccessListのマージ

https://github.com/apple/swift/blob/swift-5.9-RELEASE/stdlib/public/Observation/Sources/Observation/ObservationTracking.swift#L165

## 発表資料

https://speakerdeck.com/yimajo/swift-5-dot-9-karano-observation-haios17-wei-man-karamoshi-ete-struct-nobian-geng-jian-zhi-modekirunsukane