9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-02-04

はじめに

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によるトラッキングが可能なのです。

サンプルコード

9
8
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
9
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?