Composable Architecture (略して TCA) は、構成、テスト、人間工学を念頭に置いて、一貫性のあるわかりやすい方法でアプリケーションを構築するためのライブラリです。 SwiftUI、UIKit など、および任意の Apple プラットフォーム (iOS、macOS、visionOS、tvOS、watchOS) で使用できます。
コンポーザブル アーキテクチャとは何ですか?
もっと詳しく知る
例
基本的な使い方
ドキュメント
コミュニティ
インストール
翻訳
このライブラリは、さまざまな目的と複雑さのアプリケーションを構築するために使用できるいくつかのコア ツールを提供します。アプリケーションを構築する際に日常的に遭遇する次のような多くの問題を解決するために従うことができる説得力のあるストーリーを提供します。
状態管理
単純な値型を使用してアプリケーションの状態を管理し、多くの画面間で状態を共有して、ある画面での変更を別の画面ですぐに観察できるようにする方法。
構成
大きな機能を、それぞれ独立したモジュールに抽出して、簡単に貼り合わせて機能を形成できる小さなコンポーネントに分割する方法。
副作用
可能な限り最もテストしやすく理解しやすい方法で、アプリケーションの特定の部分を外部の世界と通信させる方法。
テスト
アーキテクチャに組み込まれた機能をテストするだけでなく、多くの部分で構成された機能の統合テストを作成し、副作用がアプリケーションにどのような影響を与えるかを理解するためのエンドツーエンドのテストを作成する方法。これにより、ビジネス ロジックが期待どおりに実行されていることを強力に保証できます。
人間工学
できるだけ少ない概念と可動部分を使用して、シンプルな API で上記のすべてを実現する方法。
コンポーザブル アーキテクチャは、Brandon Williams と Stephen Celis がホストを務める関数型プログラミングと Swift 言語を探求するビデオ シリーズである Point-Free の多くのエピソードを通じて設計されました。
ここですべてのエピソードを視聴できるだけでなく、建築をゼロから説明する複数部構成の専用ツアーも視聴できます。
このリポジトリには、コンポーザブル アーキテクチャに関する一般的で複雑な問題を解決する方法を示すサンプルが多数含まれています。このディレクトリをチェックして、以下を含むすべてのファイルを確認してください。
ケーススタディ
はじめる
効果
ナビゲーション
高次レデューサー
再利用可能なコンポーネント
ロケーションマネージャー
モーションマネージャー
検索
音声認識
同期アップアプリ
三目並べ
トドス
ボイスメモ
もっと充実したものをお探しですか? SwiftUI とコンポーザブル アーキテクチャで構築された iOS 単語検索ゲーム、isowords のソース コードを確認してください。
注記
ステップバイステップのインタラクティブなチュートリアルについては、「コンポーザブル アーキテクチャの紹介」を必ずチェックしてください。
コンポーザブル アーキテクチャを使用して機能を構築するには、ドメインをモデル化するいくつかの型と値を定義します。
State : 機能がロジックを実行し、UI をレンダリングするために必要なデータを記述するタイプ。
Action : ユーザーアクション、通知、イベントソースなど、機能内で発生する可能性のあるすべてのアクションを表すタイプ。
Reducer : アクションが与えられた場合に、アプリの現在の状態を次の状態に進化させる方法を記述する関数。リデューサーは、API リクエストなど、実行する必要があるエフェクトを返す役割も果たします。これは、 Effect
値を返すことによって実行できます。
Store : 実際に機能を駆動するランタイム。すべてのユーザー アクションをストアに送信すると、ストアでリデューサーとエフェクトを実行できるようになり、ストア内の状態の変化を観察して UI を更新できるようになります。
これを行う利点は、機能のテスト可能性が即座に解放され、大規模で複雑な機能を結合できる小さなドメインに分割できることです。
基本的な例として、数値を増減する「+」および「-」ボタンとともに数値を表示する UI を考えてみましょう。話を面白くするために、タップするとその数値に関するランダムな事実を取得してビューに表示する API リクエストを行うボタンがあるとします。
この機能を実装するには、機能のドメインと動作を格納する新しい型を作成し、 @Reducer
マクロで注釈を付けます。
import ComposableArchitecture@Reducerstruct 機能 {}
ここでは、現在のカウントの整数と、表示されている事実を表すオプションの文字列で構成される機能の状態のタイプを定義する必要があります。
@Reducerstruct 機能 { @ObservableState struct State: Equatable { var count = 0 varnumberFact: String? }}
注記
ライブラリ内の観察ツールを活用するために、 @ObservableState
マクロをState
に適用しました。
また、機能のアクションのタイプを定義する必要があります。デクリメント ボタン、インクリメント ボタン、ファクト ボタンのタップなどの明らかなアクションがあります。ただし、ファクト API リクエストからの応答を受け取ったときに発生するアクションなど、少しわかりにくいものもいくつかあります。
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { case decrementButtonTapped case incrementButtonTapped ケース番号FactButtonTapped ケース番号FactResponse(String) }}
次に、機能の実際のロジックと動作を構成するbody
プロパティを実装します。この中でReduce
リデューサーを使用して、現在の状態を次の状態に変更する方法と、どのようなエフェクトを実行する必要があるかを記述することができます。一部のアクションはエフェクトを実行する必要がなく、それを表すために.none
を返すことができます。
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { /* ... */ } var body: some Reducer{ Reduce { state, action in スイッチアクション { case .decrementButtonTapped: state.count -= 1 return .none case .incrementButtonTapped: state.count += 1 return .none case .numberFactButtonTapped: return .run { [count = state.count] send in let (data, _) = try await URLSession.shared.data( from: URL(文字列: "http://numbersapi.com/(count)/trivia")! ) await send( .numberFactResponse(String(decoding: data, as: UTF8.self)) ) } case let .numberFactResponse(fact): state.numberFact = ファクトを返す .none } } }}
そして最後に、フィーチャを表示するビューを定義します。これは、状態に対するすべての変更を監視して再レンダリングできるようにStoreOf
を保持します。また、状態が変化するようにすべてのユーザー アクションをストアに送信できます。
struct FeatureView: View { let store: StoreOfvar body: some View { Form { Section { Text("(store.count)") Button("Decrement") { store.send(.decrementButtonTapped) } Button( "インクリメント") { store.send(.incrementButtonTapped) } } セクション { Button("数値ファクト") {ストア.send(.numberFactButtonTapped) } } if let fat = store.numberFact { Text(fact) } } }}
このストアから UIKit コントローラーを駆動することも簡単です。 viewDidLoad
でストア内の状態の変化を観察し、ストアからのデータを UI コンポーネントに設定できます。コードは SwiftUI バージョンより少し長いため、ここでは折りたたんであります。
class FeatureViewController: UIViewController { let store: StoreOfinit(store: StoreOf ) { self.store = store super.init(nibName: nil, Bundle: nil) } required init?(coder: NSCoder) { FatalError("init(coder:) が実装されていません") } override func viewDidLoad() { super.viewDidLoad() let countLabel = UILabel() let decrementButton = UIButton() let incrementButton = UIButton() let fatLabel = UILabel() // 省略: サブビューの追加と制約の設定... { [弱い自分] を観察してください ガードさせて自分を守る それ以外の場合は { 戻り値 } countLabel.text = "(self.store.text)" fatLabel.text = self.store.numberFact } } @objc private func incrementButtonTapped() { self.store.send(.incrementButtonTapped) } @objc private func decrementButtonTapped() { self.store.send(.decrementButtonTapped) } @objc private func fattonTapped() { self.store.send(.numberFactButtonTapped) }}
たとえばアプリのエントリ ポイントでこのビューを表示する準備ができたら、ストアを構築できます。これは、アプリケーションを開始する初期状態と、アプリケーションに電力を供給するリデューサーを指定することで実行できます。
import ComposableArchitecture@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( ストア: Store(initialState:Feature.State()) {Feature() } ) } }}
画面上に何かを表示して遊ぶには、これで十分です。通常の SwiftUI の方法でこれを行う場合よりも確かに手順がいくつか増えますが、いくつかの利点があります。これにより、一部の監視可能なオブジェクトや UI コンポーネントのさまざまなアクション クロージャにロジックを分散させるのではなく、一貫した方法で状態の変更を適用できるようになります。また、副作用を簡潔に表現する方法も提供します。そして、多くの追加作業を行わずに、エフェクトを含むこのロジックをすぐにテストできます。
注記
テストの詳細については、専用のテスト記事を参照してください。
テストするにはTestStore
を使用します。これはStore
と同じ情報を使用して作成できますが、アクションが送信されるにつれて機能がどのように進化するかをアサートできるようにするための追加の作業が行われます。
@Testfunc Basics() async { let store = TestStore(initialState:Feature.State()) {Feature() }}
テスト ストアが作成されたら、それを使用してユーザーのステップ フロー全体のアサーションを作成できます。その状態を証明するために必要な各段階で、期待どおりに変化しました。たとえば、増分ボタンと減分ボタンをタップするユーザー フローをシミュレートできます。
// 増加/減少ボタンをタップすると countawait が変化することをテストします store.send(.incrementButtonTapped) { $0.count = 1}await store.send(.decrementButtonTapped) { $0.カウント = 0}
さらに、ステップによってエフェクトが実行され、データがストアにフィードバックされる場合は、それについてアサートする必要があります。たとえば、ユーザーがファクト ボタンをタップすることをシミュレートすると、ファクトを含むファクト応答を受け取ることが期待され、これにより、 numberFact
状態が設定されます。
awaitstore.send(.numberFactButtonTapped)awaitstore.receive(.numberFactResponse) { $0.numberFact = ???}
しかし、どのような事実が私たちに送り返されるのかをどうやって知ることができるでしょうか?
現在、私たちのリデューサーは、現実世界に到達して API サーバーにアクセスするエフェクトを使用しています。つまり、その動作を制御する方法がありません。このテストを作成するために、インターネット接続と API サーバーの可用性を気まぐれに調整しています。
デバイス上でアプリケーションを実行するときはライブ依存関係を使用できますが、テストにはモックされた依存関係を使用できるように、この依存関係をリデューサーに渡すことをお勧めします。これを行うには、 Feature
Reducer にプロパティを追加します。
@Reducerstruct 機能 { letnumberFact: (Int) 非同期スロー -> 文字列 // ...}
次に、 reduce
実装でそれを使用できます。
case .numberFactButtonTapped: return .run { [count = state.count] 送信 let fat = try await self.numberFact(count) await send(.numberFactResponse(fact)) }
そして、アプリケーションのエントリ ポイントで、実際に現実世界の API サーバーと対話するバージョンの依存関係を提供できます。
@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( ストア:Store(initialState:Feature.State()) {Feature( numberFact: { let (data, _) の数値 = URLSession.shared.data( を待ってください) from: URL(文字列: "http://numbersapi.com/(number)")! ) return String(デコード: データ、as: UTF8.self) } ) } ) } }}
しかし、テストでは、決定的で予測可能な事実を即座に返す疑似依存関係を使用できます。
@Testfunc Basics() async { let store = TestStore(initialState:Feature.State()) {Feature(numberFact: { "($0) は良い数字です。ブレント" }) }}
この少しの事前作業により、ユーザーがファクト ボタンをタップすることをシミュレートし、依存関係からファクトを提示する応答を受け取ることでテストを完了できます。
awaitstore.send(.numberFactButtonTapped)awaitstore.receive(.numberFactResponse) { $0.numberFact = "0 は良い数字です ブレント"}
アプリケーションでnumberFact
依存関係を使用する際の人間工学を改善することもできます。時間の経過とともに、アプリケーションは多くの機能に進化する可能性があり、それらの機能の一部は、 numberFact
へのアクセスも必要とする場合があり、それをすべてのレイヤーに明示的に渡すのは煩わしい場合があります。依存関係をライブラリに「登録」するプロセスがあり、依存関係をアプリケーション内のどのレイヤーでも即座に利用できるようになります。
注記
依存関係管理の詳細については、専用の依存関係に関する記事を参照してください。
まず、数値ファクト機能を新しい型でラップすることから始めます。
struct NumberFactClient { var fetch: (Int) 非同期スロー -> String}
次に、クライアントをDependencyKey
プロトコルに準拠させることで、その型を依存関係管理システムに登録します。これには、シミュレーターまたはデバイスでアプリケーションを実行するときに使用するライブ値を指定する必要があります。
拡張機能 NumberFactClient:DependencyKey { static let liveValue = Self( fetch: {number in let (data, _) = try await URLSession.shared .data(from: URL(string: "http://numbersapi.com/(number)")! ) return String(decoding: data, as: UTF8.self) } )}extension dependencyValues { var numberFact: NumberFactClient { get { self[NumberFactClient.self] } set { self[NumberFactClient.self] = newValue } }}
この少しの事前作業が完了したら、 @Dependency
プロパティ ラッパーを使用して、任意の機能で依存関係の利用をすぐに開始できます。
@レデューサー struct 機能 {- letnumberFact: (Int) async throws -> String+ @Dependency(.numberFact) varnumberFact …- self.numberFact(count) を待ってみる+ self.numberFact.fetch(count) を待ってみる }
このコードは以前とまったく同じように機能しますが、機能のリデューサーを構築するときに依存関係を明示的に渡す必要がなくなりました。プレビュー、シミュレーター、またはデバイス上でアプリを実行する場合、ライブ依存関係がリデューサーに提供され、テストではテスト依存関係が提供されます。
これは、アプリケーションへのエントリ ポイントが依存関係を構築する必要がなくなったことを意味します。
@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( ストア: Store(initialState:Feature.State()) {Feature() } ) } }}
また、依存関係を指定せずにテスト ストアを構築できますが、テストの目的で必要な依存関係をオーバーライドすることもできます。
let store = TestStore(initialState:Feature.State()) {Feature()} withDependency:{ $0.numberFact.fetch = { "($0) は良い数字です。ブレント" }}// ...
これが、コンポーザブル アーキテクチャでの機能の構築とテストの基本です。構成、モジュール性、適応性、複雑な効果など、探求すべきことは他にもたくさんあります。 Examples ディレクトリには、より高度な使用法を確認するために探索できるプロジェクトが多数含まれています。
リリースとmain
のドキュメントはここから入手できます。
main
1.17.0 (移行ガイド)
1.16.0 (移行ガイド)
1.15.0 (移行ガイド)
1.14.0 (移行ガイド)
1.13.0 (移行ガイド)
1.12.0 (移行ガイド)
1.11.0 (移行ガイド)
1.10.0 (移行ガイド)
1.9.0 (移行ガイド)
1.8.0 (移行ガイド)
1.7.0 (移行ガイド)
1.6.0 (移行ガイド)
1.5.0 (移行ガイド)
1.4.0 (移行ガイド)
1.3.0
1.2.0
1.1.0
1.0.0
0.59.0
0.58.0
0.57.0
ドキュメントには、ライブラリに慣れるにつれて役立つ記事が多数含まれています。
はじめる
依存関係
テスト
ナビゲーション
共有状態
パフォーマンス
同時実行性
バインディング
コンポーザブル アーキテクチャについて議論したい場合、またはそれを使用して特定の問題を解決する方法について質問がある場合は、他の Point-Free 愛好家と議論できる場所がいくつかあります。
長い形式のディスカッションについては、このリポジトリのディスカッション タブをお勧めします。
カジュアルなチャットならポイントフリーコミュニティslackがおすすめです。
ComposableArchitecture をパッケージの依存関係として追加することで、Xcode プロジェクトに追加できます。
「ファイル」メニューから「パッケージ依存関係の追加...」を選択します。
パッケージ リポジトリ URL テキスト フィールドに「https://github.com/pointfreeco/swift-composable-architecture」と入力します。
プロジェクトの構造に応じて次のようになります。
ライブラリにアクセスする必要があるアプリケーション ターゲットが 1 つだけある場合は、 ComposableArchitecture をアプリケーションに直接追加します。
このライブラリを複数の Xcode ターゲットから使用する場合、または Xcode ターゲットと SPM ターゲットを混合する場合は、 ComposableArchitectureに依存する共有フレームワークを作成し、すべてのターゲットでそのフレームワークに依存する必要があります。この例については、三目並べデモ アプリケーションをチェックしてください。このデモ アプリケーションは、多くの機能をモジュールに分割し、三目並べSwift パッケージを使用してこの方法で静的ライブラリを使用します。
コンポーザブル アーキテクチャは拡張性を念頭に置いて構築されており、アプリケーションを強化するために利用できるコミュニティでサポートされているライブラリが多数あります。
Composable Architecture Extras: Composable Architecture のコンパニオン ライブラリです。
TCAComposer: コンポーザブル アーキテクチャで定型コードを生成するためのマクロ フレームワーク。
TCACordinators: コンポーザブル アーキテクチャのコーディネーター パターン。
ライブラリに貢献したい場合は、そのライブラリへのリンクを含む PR を開いてください。
この README の次の翻訳は、コミュニティのメンバーによって提供されました。
アラビア語
フランス語
ヒンディー語
インドネシア語
イタリア語
日本語
韓国人
研磨
ポルトガル語
ロシア
簡体字中国語
スペイン語
ウクライナ語
翻訳に貢献したい場合は、Gist へのリンクを含む PR を開いてください。
図書館に関してよく寄せられる質問やコメントすべてに特化した記事を用意しています。
次の人々が初期段階でライブラリにフィードバックを提供し、現在のライブラリの完成に貢献しました。
ポール・コルトン、カーン・デデオグル、マット・ディープハウス、ジョセフ・ドレジャル、エイマンタス、マシュー・ジョンソン、ジョージ・カイマカス、ニキータ・レオノフ、クリストファー・リスシオ、ジェフリー・マッコ、アレハンドロ・マルティネス、シャイ・ミシャリ、ウィリス・プラマー、サイモン=ピエール・ロイ、ジャスティン・プライス、スヴェン・A・シュミット、カイル・シャーマン、ペトル・シーマ、 Jasdev Singh、Maxim Smirnov、Ryan Stone、Daniel Hollis Tavares、およびすべてのポイントフリー購読者?
SwiftUI の多くの奇妙な癖に対処し、最終的な API を改良するのに協力してくれた Chris Liscio に特に感謝します。
そして、Shai Mishali と CombineCommunity プロジェクトのおかげで、私たちはPublishers.Create
の実装をそこから取り入れました。これをEffect
に使用して、デリゲートとコールバックベースの API の橋渡しを支援し、サードパーティのフレームワークとのインターフェイスをより簡単にします。
コンポーザブル アーキテクチャは、他のライブラリ、特に Elm と Redux によって開始されたアイデアの基盤に基づいて構築されました。
Swift および iOS コミュニティには、多くのアーキテクチャ ライブラリもあります。これらのそれぞれには、コンポーザブル アーキテクチャとは異なる独自の優先順位とトレードオフがあります。
RIB
ループ
リスイフト
ワークフロー
リアクターキット
受信フィードバック
メビウス.スウィフト
フラクサー
約束された建築キット
このライブラリは MIT ライセンスに基づいてリリースされています。詳細については、「ライセンス」を参照してください。