はじめに
TCA 1.7.0でObservationフレームワークが導入され、SwiftUIビューではViewStoreを使う必要がなくなるなど実装方法が劇的に変わりました。
UIKit側にも変更があり、Stateの変更をトラッキングするための新しい仕組みが導入されています。
本記事では、まず1.7.0で新しく導入されたobserve(_:)メソッドの使い方について解説し、その後observe(_:)
メソッドがどのようにStateの変更のトラッキングを行っているのかを解説します。
1.7.0以前の実装方法
1.7.0以前では、以下のようにStore.publisher
またはViewStore.publisher
をサブスクライブすることで状態の更新を検知していました。
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)
}
複数のプロパティの変更を監視したい場合は、以下のようにサブスクライブする実装を複数記述するのが一般的です。
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(_:)
メソッドを利用して状態更新をトラッキングします。
override func viewDidLoad() {
// ...
let countLabel = UILabel()
observe { [weak self] in
guard let self else { return }
countLabel.text = "\(store.count)"
}
}
クロージャ内で参照しているStateのプロパティに変更があったときに、このクロージャが再び実行されるようになっています。
observe(_:)
を利用する場合、複数プロパティのトラッキングも同じクロージャに記述するのが一般的です。
Store.publisher
と比較して、より少ないコード量で記述できるようになりました。
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をリロードする処理が実行されてしまい非効率です。
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(_:)
を分けて実装します。
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が参考になります。
ナビゲーションなどもobserve(_:)
メソッドで行われています。
observe(_:)
が状態変更をトラッキングする仕組み
observe(_:)メソッドの中身を覗いてみましょう。
以下は1.7.0時点での実装です。
@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:)に置き換えて考えれば大丈夫です。
withObservationTracking(_:onChange:)の概要
withObservationTracking(_:onChange:)
はクロージャを2つ受け取ります。
最初のクロージャでObservable
に準拠した型のプロパティを参照すると、そのプロパティがトラッキング対象となります。
そして、トラッキング対象のプロパティの値が更新されると2つ目のクロージャが呼ばれます。
@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を再度呼び出すようにする必要があります。
override func viewDidLoad() {
func track() {
withObservationTracking {
_ = self.model.count
} onChange: {
Task { @MainActor in
print("\(self.model.count)")
track()
}
}
}
track()
}
前述したobserve(_:)
メソッドと同じような形になりましたね?
observe(_:)
を使った実装をもう一度見てみる
もう一度以下の実装を見てみましょう。
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によるトラッキングが可能なのです。
サンプルコード