【TCA】UIKitでStateの変更をトラッキングするobserve(_:)メソッドの仕組み

# はじめに
TCA 1.7.0でObservationフレームワークが導入され、SwiftUIビューではViewStoreを使う必要がなくなるなど実装方法が劇的に変わりました。
UIKit側にも変更があり、Stateの変更をトラッキングするための新しい仕組みが導入されています。

本記事では、まず1.7.0で新しく導入された[observe(_:)](https://github.com/pointfreeco/swift-composable-architecture/blob/1.7.0/Sources/ComposableArchitecture/UIKit/NSObject%2BObservation.swift#L141)メソッドの使い方について解説し、その後`observe(_:)`メソッドがどのようにStateの変更のトラッキングを行っているのかを解説します。

# 1.7.0以前の実装方法
1.7.0以前では、以下のように`Store.publisher`または`ViewStore.publisher`をサブスクライブすることで状態の更新を検知していました。

```swift
override func viewDidLoad() {
  // ...
  let countLabel = UILabel()

  store.publisher.count
    .sink { [weak self] count in
      guard let self else { return }
      countLabel.text = "\(count)"
    }
    .store(in: &cancellables)
}
```

複数のプロパティの変更を監視したい場合は、以下のようにサブスクライブする実装を複数記述するのが一般的です。

```swift
override func viewDidLoad() {
  // ...
  let count1Label = UILabel()
  let count2Label = UILabel()

  store.publisher.count1
    .sink { [weak self] count in
      guard let self else { return }
      count1Label.text = "\(count)"
    }
    .store(in: &cancellables)

  store.publisher.count2
    .sink { [weak self] count in
      guard let self else { return }
      countLabel2.text = "\(count)"
    }
    .store(in: &cancellables)
}
```

# 1.7.0以降の実装方法
1.7.0以降では、`observe(_:)`メソッドを利用して状態更新をトラッキングします。

```swift
override func viewDidLoad() {
  // ...
  let countLabel = UILabel()

  observe { [weak self] in
    guard let self else { return }
    countLabel.text = "\(store.count)"
  }
}
```

クロージャ内で参照しているStateのプロパティに変更があったときに、このクロージャが再び実行されるようになっています。

`observe(_:)`を利用する場合、複数プロパティのトラッキングも同じクロージャに記述するのが一般的です。
`Store.publisher`と比較して、より少ないコード量で記述できるようになりました。

```swift
override func viewDidLoad() {
  // ...
  let countLabel = UILabel()
  let countLabel2 = UILabel()

  observe { [weak self] in
    guard let self else { return }
    countLabel1.text = "\(store.count1)"
    countLabel2.text = "\(store.count2)"
  }
}
```

# 注意点
`observe(_:)`を利用する上で注意が必要な点があります。

UITableViewに表示しているアイテムに更新があったときに、UITableViewをリロードする処理があるとします。
この処理と`countLabel`を更新する処理を同じクロージャに実装してしまうと、`count`に変更があったときもUITableViewをリロードする処理が実行されてしまい非効率です。

```swift
override func viewDidLoad() {
  // ...
  let countLabel = UILabel()

  observe { [weak self] in
    guard let self else { return }

    countLabel.text = "\(store.count)"    
    
    self.dataSource = store.items
    self.tableView.reloadData()
  }
}
```

このようなケースでは、`observe(_:)`を分けて実装します。

```swift
override func viewDidLoad() {
  // ...
  let countLabel = UILabel()

  observe { [weak self] in
    guard let self else { return }
    countLabel.text = "\(store.count)"    
  }

  observe { [weak self] in
    guard let self else { return }
    self.dataSource = store.items
    self.tableView.reloadData()
  }
}
```

# 公式サンプルが参考になる
公式サンプルである[TicTacToe](https://github.com/pointfreeco/swift-composable-architecture/blob/main/Examples/TicTacToe/tic-tac-toe/Sources/LoginUIKit/LoginViewController.swift#L93)が参考になります。
ナビゲーションなども`observe(_:)`メソッドで行われています。

# `observe(_:)`が状態変更をトラッキングする仕組み
[observe(_:)](https://github.com/pointfreeco/swift-composable-architecture/blob/1.7.0/Sources/ComposableArchitecture/UIKit/NSObject%2BObservation.swift#L141)メソッドの中身を覗いてみましょう。

以下は1.7.0時点での実装です。

```swift
@discardableResult
public func observe(_ apply: @escaping () -> Void) -> ObservationToken {
  let token = ObservationToken()
  self.tokens.insert(token)
  @Sendable func onChange() {
    guard !token.isCancelled
    else { return }

    withPerceptionTracking(apply) {
      Task { @MainActor in
        guard !token.isCancelled
        else { return }
        onChange()
      }
    }
  }
  onChange()
  return token
}
```

`withPerceptionTracking`は、iOS17以上か未満かでObservationの実装を分岐します。
iOS17以上であればSwiftのオフィシャルなObservationの実装が使用され、iOS17未満であればバックポートされたObservationの実装が使用されます。

ここではあまり細かいことは気にせず、[withObservationTracking(_:onChange:)](https://developer.apple.com/documentation/observation/withobservationtracking(_:onchange:))に置き換えて考えれば大丈夫です。

## withObservationTracking(_:onChange:)の概要
`withObservationTracking(_:onChange:)`はクロージャを2つ受け取ります。

最初のクロージャで`Observable`に準拠した型のプロパティを参照すると、そのプロパティがトラッキング対象となります。
そして、トラッキング対象のプロパティの値が更新されると2つ目のクロージャが呼ばれます。

```swift
@Observable
class CounterModel {
  var count = 0
}

override func viewDidLoad() {
  withObservationTracking {
    _ = self.model.count
  } onChange: {
    Task { @MainActor in
      print("\(self.model.count)")
    }
  }
}
```

ただし、withObservationTrackingは状態変化を1回検知したら値のトラッキングを終了してしまいます。
継続的に状態変化をトラッキングするには、以下のようにonChangeクロージャでwithObservationTrackingを再度呼び出すようにする必要があります。

```swift
override func viewDidLoad() {
  func track() {
    withObservationTracking {
      _ = self.model.count
    } onChange: {
      Task { @MainActor in
        print("\(self.model.count)")
        track()
      }
    }
  }
  track()
}
```

前述した`observe(_:)`メソッドと同じような形になりましたね？

## `observe(_:)`を使った実装をもう一度見てみる
もう一度以下の実装を見てみましょう。

```swift
override func viewDidLoad() {
  // ...
  let countLabel = UILabel()

  observe { [weak self] in
    guard let self else { return }
    countLabel.text = "\(store.count)"
  }
}
```

`observe(_:)`に渡しているクロージャは、`withObservationTracking(_:onChange:)`の最初の引数に渡されます。
つまり、ここで参照しているプロパティがトラッキング対象となります。
TCA1.7.0からStoreは`Observable`プロトコルに準拠した型になっているので、withObservationTrackingによるトラッキングが可能なのです。

# サンプルコード
https://github.com/takehilo/MyiOSPlayground/tree/main/TCAUIKitObserve