【UIKit版】The Composable Architecture(TCA)のカウンターサンプルを理解する

f:id:bamboohero:20210601012902p:plain

TCAはSwiftUIとの親和性が高いですが、UIKitでも実装が可能です。

TCAではUIKitによる実装サンプルも提供されているので、今回はUIKit版のカウンターサンプルを使ってUIKitでTCAを実装する方法について学びます。

UIKitCaseStudies/CounterViewController


SwiftUI版のカウンターサンプルの解説はこちらをご参照ください。

bamboo-hero.com



State、Action、Environment、Reducerの実装はSwiftUI版と全く同じ

SwiftUI版のカウンターサンプルと見比べるとわかるのですが、StateなどのTCAの各コンポーネントの実装は全く同じです。

これらのコンポーネントがUIKitでもSwiftUIでも同じように実装できるということは、UIKitで作られたアプリにあとからSwiftUIとTCAを導入して連携させることも比較的容易なのではないかと妄想しています(まだやったことないのでどのくらいの難易度なのか、そもそもできるのかとかはわかってません)。


StateのサブスクライブとActionのトリガーの設定

UIKitのビュー(UIViewController)でStateをサブスクライブする方法とActionのトリガーを設定する方法について見ていきます。

どちらもViewStoreを介して行うのはSwiftUI版と同じです。UIViewControllerのイニシャライザでStoreを受け取り、ViewStoreを生成してUIViewControllerのプロパティに格納しておきます。

final class CounterViewController: UIViewController {
  let viewStore: ViewStore<CounterState, CounterAction>
  var cancellables: Set<AnyCancellable> = []

  init(store: Store<CounterState, CounterAction>) {
    self.viewStore = ViewStore(store)
    super.init(nibName: nil, bundle: nil)
  }


Stateをサブスクライブするときは、ViewStoreのpublisherプロパティを参照します。ここではpublisherはCounterState型のOutputを持っています。

self.viewStore.publisher
  .map { "\($0.count)" }
  .assign(to: \.text, on: countLabel)
  .store(in: &self.cancellables)


カウンターのプラスボタンタップ時にActionをトリガーする処理は典型的なTarget-Actionパターンで実装されています。

incrementButton.addTarget(self, action: #selector(incrementButtonTapped), for: .touchUpInside)

...

@objc func incrementButtonTapped() {
  self.viewStore.send(.incrementButtonTapped)
}


Actionが処理される流れ

Actionがトリガーされたあとの流れはSwiftUI版と同じなので割愛します。

こちらの記事をご参照ください。

bamboo-hero.com


まとめ

ざっとですがカウンターサンプルを例にUIKitでTCAを実装する方法について説明しました。

TCAはSwiftUIでもUIKitでもどちらでも実装できるように考慮されているので、UIKitで作られたアプリにあとからSwiftUIとTCAを導入し、Stateなどは両方のアーキテクチャで共有するといったことができそうです。

私の観測範囲ではそういった事例はまだ出てきて無さそうですが、今後TCAが広まって様々な導入事例が出てくることに期待したいですね。