2026年01月20日
こんにちは!新入社員のYです。
今回の技術ブログではタイトルの通り、the Composable Architecture(TCA)の紹介と簡単なコードを用いて、入門のようなことをやっていきたいと思います。
まず初めに、冒頭でも触れましたが、TCAはthe Composable Architecture の略称です。
TCAは後述のState、Action、ReducerというReduxでお馴染みの構成要素を用いて、SwiftUIアプリの状態管理と副作用を予測可能な形で制御するためのライブラリ群です。以下のURLはTCAの公式Githubです。
GithubURL: https://github.com/pointfreeco/swift-composable-architecture
本記事ではなぜTCAを採用するのかといった問題の詳細については触れないので、ご興味がある方は以下の記事をご参照ください。
https://qiita.com/karamage/items/f63a5750e65c5c9745ae
以下ではTCAの解説でよくあるコードを用いて、TCAを実際に動かしてみます。お手元のXcodeでTCAを体験したい場合には、Swift Package Managerでhttps://github.com/pointfreeco/swift-composable-architectureのURLを用いて、インストールを行なってください。
TCAではState、Action、Reducerの3つの構成要素が存在します。
TCAにおいて、Stateはその画面が持つべき「状態」をひとまとめにした構造体です。 例えば、「現在のカウント数」や「ログインしているかどうか」といった、Viewが表示や挙動を決定するために必要なデータは、すべてここに定義します。
@ObservableState
struct State{
var count = 0
var isLoggedIn = false
}
Actionは、その画面で発生する「すべての出来事」を定義したものです。 ユーザーがボタンをタップした、画面が表示された、あるいはAPI通信の結果が返ってきた、といった「状態(State)を変化させるトリガー」を列挙型(enum)で管理します。
enum Action{
case decrementButtonTapped
case incrementButtonTapped
}
Reducerは、アプリのロジックを一手に引き受ける司令塔です。「現在のState」と「発生したAction」を受け取り、Stateをどう更新するか、そして副作用(Effect)を実行するかを決定します。
var body:some Reducer<State,Action>{
Reduce { state, action in
switch action {
case .incrementButtonTapped:
state.count += 1
return .none
case .decrementButtonTapped:
state.count -= 1
return .none
}
}
}
まとめたものが以下のコードです。
import ComposableArchitecture
@Reducer
@MainActor
struct HomeFeature {
@ObservableState
struct State{
var count = 0
var isLoggedIn = false
}
enum Action{
case decrementButtonTapped
case incrementButtonTapped
}
var body:some Reducer<State,Action>{
Reduce { state, action in
switch action {
case .incrementButtonTapped:
state.count += 1
return .none
case .decrementButtonTapped:
state.count -= 1
return .none
}
}
}
}
State、Action、Reducerが揃ったら、最後にそれらをSwiftUIのViewと繋ぎます。 TCAでは、ViewからStoreを通じて状態を参照したり、アクションを送ったりします。
import SwiftUI
import ComposableArchitecture
struct HomeView:View {
let store:StoreOf
var body :some View{
HStack {
Button("-") {store.send(.decrementButtonTapped)}
Text(String(store.count))
Button("+"){store.send(.incrementButtonTapped)}
}
}
}
基本が理解できたら、次は「ログイン画面からホーム画面へ」といった画面遷移の実装です。TCAでは、遷移先の状態もすべてStateで管理します。
Destination 列挙型 @Reducer enum Destination を定義することで、その画面から「どこに遷移する可能性があるか」を列挙します。これにより、複数の遷移先を安全に管理できます。
@Presents マクロ state.destination に @Presents を付与するのがポイントです。
この値が nil でなければ:画面を表示している
この値が nil になれば:画面を閉じる というように、データの状態でナビゲーションを制御します。
.ifLet 演算子 Reducerの最後にある .ifLet は、遷移先の画面(HomeFeature)で発生したアクションを、親画面(LoginFeature)でも扱えるように繋ぎこむためのマジックです。
state.destination = .home(...) ログインボタンが押された際に、遷移先の初期状態(HomeFeature.State())を代入するだけで、SwiftUI側がそれを検知して画面を切り替えます。
import ComposableArchitecture
@Reducer
@MainActor
struct LoginFeature {
@Reducer
enum Destination {
case home(HomeFeature)
}
@ObservableState
struct State {
var username = ""
var password = ""
@Presents var destination: Destination.State?
}
enum Action {
case usernameChanged(String)
case passwordChanged(String)
case loginButtonTapped
case destination(PresentationAction)
}
var body: some Reducer<State, Action> {
Reduce { state, action in
switch action {
case let .usernameChanged(u):
state.username = u
return .none
case let .passwordChanged(p):
state.password = p
return .none
case .loginButtonTapped:
// 画面遷移
state.destination = .home(HomeFeature.State())
return .none
case .destination:
return .none
}
}
.ifLet(\.$destination, action: \.destination)
}
}
最後に、ログイン画面のUIを構築します。ここでは「ユーザー入力の同期(Binding)」と「条件に応じた画面遷移」がポイントになります。
import SwiftUI
import ComposableArchitecture
struct LoginView:View {
@Bindable var store:StoreOf
var body :some View{
NavigationStack {
VStack(alignment:.center) {
Text("ログイン")
.font(.largeTitle)
.bold()
.padding(.top,16)
TextField("ユーザー名を入力", text: $store.username.sending(\.usernameChanged))
SecureField("パスワードを入力", text: $store.password.sending(\.passwordChanged))
Button("ログイン") {store.send(.loginButtonTapped)}
.foregroundColor(.red)
.padding()
.background(.blue)
.cornerRadius(10)
.overlay{
RoundedRectangle(cornerRadius:10)
.stroke(Color.blue,lineWidth: 1)
}
}
.navigationDestination(item: $store.scope(state:\.destination?.home, action:\.destination.home)
){store in
HomeView(store:store)
}
}
}
}
いかがだったでしょうか。今回のブログでは、the Composable Architecture(TCA)の実装方法について解説を行いました。TCAの導入によって状態管理が単方向に強制され、状態管理や処理の見通しが良くなりました。しかし、学習コストの高さは否めないというのが、私の所感です。きちんと身につければ大きな武器となることは間違いないと思うので、引き続き学習を進めていきたいと思います。
私自身が未熟で理解の及んでいないところもありますが、この記事が読者の方の参考になれば幸いです。
ここまでお読みいただきありがとうございました。