ComposableArchitectureを使ってみた
TCAとはこれのことだ
ComposableArchitecture
The Composable Architecture (TCA, for short) is a library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind. It can be used in SwiftUI, UIKit, and more, and on any Apple platform (iOS, macOS, tvOS, and watchOS).
📕Overview
このライブラリは、さまざまな目的と複雑さのアプリケーションを構築するために使用できるいくつかのコア ツールを提供します。 アプリケーションを構築する際に日常的に遭遇する次のような多くの問題を解決するために従うことができる説得力のあるストーリーを提供します。
状態管理(State management)
単純な値型を使用してアプリケーションの状態を管理し、多くの画面間で状態を共有して、ある画面での変更を別の画面ですぐに観察できるようにする方法。
構成(Composition)
大きな機能を、それぞれ独立したモジュールに抽出して、簡単に貼り合わせて機能を形成できる小さなコンポーネントに分割する方法。
副作用(Side effects)
可能な限り最もテストしやすく理解しやすい方法で、アプリケーションの特定の部分を外部の世界と通信させる方法。
テスト(Testing)
アーキテクチャに組み込まれた機能をテストするだけでなく、多くの部分で構成された機能の統合テストを作成し、副作用がアプリケーションにどのような影響を与えるかを理解するためのエンドツーエンドのテストを作成する方法。 これにより、ビジネス ロジックが期待どおりに実行されていることを強力に保証できます。
人間工学(Ergonomics)
できるだけ少ない概念と可動部分を使用して、シンプルな API で上記のすべてを実現する方法。
試しに使ってみよう
使ってみようとして、以下の問題が起きました!
- x-code15.2を使っております
- ライブラリを追加したが読み込めてない?
- Generalから追加したら何故か読み込めた?
- 公式のサンプルがそのまま使えない?
- .noneの箇所でデータ型のエラーが出る?
- Githubのサンプル探してカウンターをやっと作れた💦
こちらのリンクを追加する。
importでエラーが出たら、ホワイトハウスみたいな、アイコンの下のプラスボタン押して、ダイアログ出てくるので、同じアイコンを選択して、また追加する。もしかしたら、Swiftのバージョンのエラー出るかもしれないが、選択して、ダイアログが出たら、使っても問題ない選択肢を選ぶと罰のエラーが消える。調べても出てこないのでこの辺はよくわかりません?
ソースコードは、3ファイル用意して使ってます。
Reduxの設定ファイル
ここに、状態を扱う変数であるState、Enumとして使うActionは情報を保持しているオブジェクトですかね。Reduceは、Reducerなのでしょうね。変更されたら状態を返すメソッドの役割を持っています。Reactのdispatchはないようですね。sendというメソッドが内部実装を見ると同じ役割をしているようです。dispatchと書いてあるんだけどね。
@discardableResult
public func send(_ action: ViewAction) -> StoreTask {
self.store.send(action)
}
こちらがカウンターですが、状態の管理をしてくれる変数、状態を保持するオブジェクト、状態の変更を通知するメソッドが書かれています。
import ComposableArchitecture
struct CounterReducer: Reducer {
// Jboy: 機能がそのジョブを実行するために必要な状態を保持する状態タイプ (通常は構造体)
struct State {
var count = 0
}
// Jboy: ユーザーが機能内で実行できるすべてのアクションを保持するアクション タイプ (通常は列挙型)
// ヒント: アクション ケースには、incrementButtonTapped など、文字通りユーザーが UI で行うことにちなんで名前を付けるのが最善です。
enum Action {
case decrementButtonTapped
case incrementButtonTapped
}
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .decrementButtonTapped:
state.count -= 1
return .none
case .incrementButtonTapped:
state.count += 1
return .none
}
}
}
}
// Jboy: ビュー ストアでは State が同等であることが必要です
extension CounterReducer.State: Equatable {}
Reduxを使うView
こちらの画面のファイルで、ボタンを押すとカウンターが増減するコードが書かれております。WithViewStoreで画面を作るコードをラップして、クロージャーで状態を外部から渡しています。どこからでも渡せるから、グローバルステートと呼ばれているようですね。
import ComposableArchitecture
import SwiftUI
struct ContentView: View {
// Jboy: ストアは機能のランタイムを表します。 つまり、状態を更新するためにアクションを処理できるオブジェクトであり、エフェクトを実行し、それらのエフェクトからのデータをシステムにフィードすることができます。
let store: StoreOf<CounterReducer>
var body: some View {
// Jboy: ビュー ストアを構築するための軽量構文を提供する WithViewStore。
// ストア内の状態を観察するために使用されます
// 警告: 現在、observe: { $0 } を使用してストア内のすべての状態を監視していますが、通常、機能はビューで必要な状態よりもはるかに多くの状態を保持しています。 パフォーマンスの確認(https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/performance)
WithViewStore(self.store, observe: { $0 }) { viewStore in
VStack {
Text("Redux with SwiftUI")
Text("\(viewStore.count)")
.font(.largeTitle)
.padding()
.background(Color.black.opacity(0.1))
.cornerRadius(10)
HStack {
Button("-") {
viewStore.send(.decrementButtonTapped)
}
.font(.largeTitle)
.padding()
.background(Color.black.opacity(0.1))
.cornerRadius(10)
Button("+") {
viewStore.send(.incrementButtonTapped)
}
.font(.largeTitle)
.padding()
.background(Color.black.opacity(0.1))
.cornerRadius(10)
}
}
}
}
}
アプリのエントリーポイントにReduxを設定
レデューサーを引数に渡す設定が必要なので、アプリが最初に表示されるページに設定をして上げる必要があります。カウンター作るだけでこんなに複雑とは💦
import ComposableArchitecture
import SwiftUI
@main
struct TCATutorialApp: App {
// Jboy: アプリケーションを動作させるストアは 1 回だけ作成する必要があることに注意することが重要です。 ほとんどのアプリケーションでは、シーンのルートにある WindowGroup に直接作成するだけで十分です。 ただし、静的変数として保持してシーンに提供することもできます。
// チュートリアル - 最初のリデューサー
// ヒント: `_printChanges` は、リデューサーが処理するすべてのアクションをコンソールに出力し、アクションの処理後に状態がどのように変化したかを出力します。
static let store = Store(initialState: CounterReducer.State()) {
CounterReducer()
._printChanges()
}
var body: some Scene {
WindowGroup {
ContentView(store: TCATutorialApp.store)
}
}
}
これがビルドした時の動作。カウンターだから面白くないけど笑
でも難しいんですよね。
📚まとめ
Redux は、アプリケーションの状態(State)を管理および更新するためのライブラリです。アプリケーションの多くの部分で必要な State を、グローバルな State として一元管理するのに役立ちます。
Redux は、「Action」と呼ばれるイベントを使用して、アプリケーションの State を管理し、更新します。
Redux のデータの流れは次のとおりです。
- 画面からユーザーが操作を実行
- クリックをトリガーに Action が発行される
- Dispatch が Action を運ぶ
- Action が Reducer に渡される
- Reducer により Store 内の state が変更される
- view 側に state が共有される
Redux の基本設計の 1 つである Single source of truth(ソースは 1 つだけ)でもあるように、State を書き換えるのができるのはこの Reducer のみです。
🏛️Reduxの歴史について解説
歴史を調べるとFluxというワードが出てきます。
Redux と Flux は、JavaScript アプリケーションの状態管理パターンとライブラリです。Flux は Facebook によって開発されたクライアントサイド Web アプリケーション用のアーキテクチャパターンで、単一方向のデータフローを採用しています。Redux は Flux を基にして開発されたライブラリで、アプリケーション全体の状態を単一のストア(状態ツリー)で管理します。
Redux は、Flux フローという設計思想に基づいて開発されています。Redux では、アプリケーションの状態(React.js での state)を一元管理できるため、各コンポーネントから直接データを参照・取得することができます。
Redux は、コンポーネントをまたぐステートを管理する状態管理のライブラリです。アプリケーション規模が大きくなるにつれて、有力な選択肢になるでしょう。
Flux アーキテクチャの特長は次のとおりです。
シンプルなソフトウェア構造にするためのアーキテクチャである
3つの基準「コード量」「変更への耐性」「テストのしやすさ」全てが平均的なバランス
イベント発生の多い比較的大規模なソフトウェア開発に向いている
元々は、JavaScriptライブラリとして開発されましたが、モバイルでもあるんですよね。Swift、Kotlin、Flutterでも使えます。
TCA使ってみようと思ったきっかけは、SwiftUIのアーキテクチャと状態管理は、Reduxを使ったものが推奨されるとか情報を見て気になって、Reduxは難易度高そうだけど、やってみるか〜と挑戦してみようと思ったことですね。結構難しいですね。使いこなせたら面白いのでしょうけど。
Discussion