pointfreeco/swift-composable-architecture

⭐ 13,681 🍴 1,561 Swift📄 MIT License GitHubで見る →

主な特徴

  • 単純な値型による状態管理と画面間での状態共有
  • 大規模機能を小さなコンポーネントに分解し、独立したモジュールとして抽出・再結合可能
  • テスト可能で理解しやすい方法での外部世界との通信(副作用処理)
  • 単体テスト、統合テスト、エンドツーエンドテストの包括的サポート
  • 最小限のAPIと概念で上記を実現する優れた人間工学
  • @Reducer、@ObservableState、@Dependencyなどのマクロによる簡潔な記述
  • SwiftUIとUIKitの両方をサポート
  • 時間ベースの操作(デバウンス、スロットル、遅延)の制御
  • ナビゲーションの統一的な処理(シート、フルスクリーン、プッシュ、ポップオーバー)

リポジトリ解析: pointfreeco/swift-composable-architecture

基本情報

  • リポジトリ名: pointfreeco/swift-composable-architecture
  • 主要言語: Swift
  • スター数: 13,681
  • フォーク数: 1,561
  • 最終更新: 活発に更新中(v1.17+)
  • ライセンス: MIT License
  • トピックス: Architecture, Swift, SwiftUI, UIKit, State Management, Testing, Side Effects, Functional Programming, Reactive Programming

概要

一言で言うと

The Composable Architecture(TCA)は、構成可能性、テスト容易性、人間工学を重視して設計された、一貫性があり理解しやすい方法でアプリケーションを構築するためのSwiftライブラリです。

詳細説明

The Composable Architecture(TCA)は、Swift言語で書かれた、統一的で予測可能な方法でアプリケーションを構築するためのライブラリです。Elm言語とReduxアーキテクチャから着想を得て、SwiftとAppleプラットフォームに最適化された形で実装されています。単純な値型を使用した状態管理、機能の分解と合成、テスト可能な副作用処理、そして包括的なテストサポートを提供し、ビジネスロジックが期待通りに動作することを強力に保証します。SwiftUI、UIKit、その他のAppleプラットフォーム(iOS、macOS、iPadOS、visionOS、tvOS、watchOS)で使用できます。

主な特徴

  • 単純な値型による状態管理と画面間での状態共有
  • 大規模機能を小さなコンポーネントに分解し、独立したモジュールとして抽出・再結合可能
  • テスト可能で理解しやすい方法での外部世界との通信(副作用処理)
  • 単体テスト、統合テスト、エンドツーエンドテストの包括的サポート
  • 最小限のAPIと概念で上記を実現する優れた人間工学
  • @Reducer、@ObservableState、@Dependencyなどのマクロによる簡潔な記述
  • SwiftUIとUIKitの両方をサポート
  • 時間ベースの操作(デバウンス、スロットル、遅延)の制御
  • ナビゲーションの統一的な処理(シート、フルスクリーン、プッシュ、ポップオーバー)

使用方法

インストール

前提条件

  • Swift 5.9以上
  • Xcode 15以上
  • iOS 13.0+、macOS 10.15+、tvOS 13.0+、watchOS 6.0+

インストール手順

# Swift Package Manager経由でインストール
# Package.swiftに追加
dependencies: [
  .package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.17.0")
]

# または、Xcodeで追加
# 1. File → Add Package Dependencies...
# 2. URL: https://github.com/pointfreeco/swift-composable-architecture
# 3. ComposableArchitectureを選択してターゲットに追加

基本的な使い方

Hello World相当の例

import ComposableArchitecture
import SwiftUI

// Reducerマクロを使用してFeatureを定義
@Reducer
struct Counter {
  // 状態を定義(ObservableStateマクロで観察可能に)
  @ObservableState
  struct State: Equatable {
    var count = 0
  }
  
  // アクションを定義
  enum Action {
    case incrementButtonTapped
    case decrementButtonTapped
  }
  
  // ロジックを実装
  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
      }
    }
  }
}

// Viewを定義
struct CounterView: View {
  let store: StoreOf<Counter>
  
  var body: some View {
    HStack {
      Button("-") { store.send(.decrementButtonTapped) }
      Text("\(store.count)")
      Button("+") { store.send(.incrementButtonTapped) }
    }
  }
}

// アプリのエントリーポイント
@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      CounterView(
        store: Store(initialState: Counter.State()) {
          Counter()
        }
      )
    }
  }
}

実践的な使用例

// 副作用を含む例:数値に関する豆知識をAPIから取得
@Reducer
struct NumberTrivia {
  @ObservableState
  struct State: Equatable {
    var count = 0
    var numberFact: String?
    var isLoadingFact = false
  }
  
  enum Action {
    case incrementButtonTapped
    case decrementButtonTapped
    case numberFactButtonTapped
    case numberFactResponse(Result<String, Error>)
  }
  
  // 依存性注入
  @Dependency(\.numberFact) var numberFact
  
  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
        
      case .numberFactButtonTapped:
        state.isLoadingFact = true
        state.numberFact = nil
        
        // 非同期の副作用を実行
        return .run { [count = state.count] send in
          let result = await Result {
            try await self.numberFact.fetch(count)
          }
          await send(.numberFactResponse(result))
        }
        
      case let .numberFactResponse(.success(fact)):
        state.isLoadingFact = false
        state.numberFact = fact
        return .none
        
      case .numberFactResponse(.failure):
        state.isLoadingFact = false
        state.numberFact = "エラーが発生しました"
        return .none
      }
    }
  }
}

高度な使い方

// 複数の機能を組み合わせる例
@Reducer
struct AppFeature {
  @ObservableState
  struct State: Equatable {
    var counter = Counter.State()
    var profile = Profile.State()
    @Presents var destination: Destination.State?
  }
  
  enum Action {
    case counter(Counter.Action)
    case profile(Profile.Action)
    case destination(PresentationAction<Destination.Action>)
    case settingsButtonTapped
  }
  
  var body: some Reducer<State, Action> {
    Scope(state: \.counter, action: \.counter) {
      Counter()
    }
    Scope(state: \.profile, action: \.profile) {
      Profile()
    }
    .ifLet(\.$destination, action: \.destination) {
      Destination()
    }
    
    Reduce { state, action in
      switch action {
      case .settingsButtonTapped:
        state.destination = .settings(Settings.State())
        return .none
        
      case .counter, .profile, .destination:
        return .none
      }
    }
  }
}

// テストコード
@Test
func counterIncrements() async {
  let store = TestStore(initialState: Counter.State()) {
    Counter()
  }
  
  await store.send(.incrementButtonTapped) {
    $0.count = 1
  }
  
  await store.send(.incrementButtonTapped) {
    $0.count = 2
  }
}

ドキュメント・リソース

公式ドキュメント

サンプル・デモ

  • Examples/CaseStudies/: 基礎から応用までの様々なケーススタディ
  • Examples/TicTacToe/: モジュール化されたTic-Tac-Toeゲーム
  • Examples/Todos/: Todoアプリケーション
  • Examples/VoiceMemos/: 音声メモアプリ(複雑な副作用の例)
  • Examples/SyncUps/: 完全なアプリケーションの例

チュートリアル・ガイド

  • Getting Started: 基本概念と最初のアプリ構築
  • Dependencies: 依存性管理の詳細
  • Testing: テスト戦略とベストプラクティス
  • Navigation: ナビゲーションパターン
  • Performance: パフォーマンス最適化

技術的詳細

アーキテクチャ

全体構造

TCAは単方向データフローアーキテクチャを採用。アプリケーションは4つの主要な概念で構成される:

  1. State: アプリケーションの状態を表す単純な値型
  2. Action: 状態を変更できるすべてのイベントを表す列挙型
  3. Reducer: 現在の状態とアクションから次の状態を計算し、実行すべき副作用を返す純粋関数
  4. Store: Reducerと副作用を実行し、状態の変更を観察可能にするランタイム

ディレクトリ構成

swift-composable-architecture/
├── Sources/
│   ├── ComposableArchitecture/    # メインライブラリ
│   │   ├── Effect.swift           # 副作用の抽象化
│   │   ├── Reducer.swift          # Reducer プロトコルと実装
│   │   ├── Store.swift            # Store 実装
│   │   ├── ViewStore.swift        # View バインディング
│   │   ├── Dependencies/          # 依存性注入システム
│   │   ├── SwiftUI/               # SwiftUI 統合
│   │   ├── UIKit/                 # UIKit 統合
│   │   └── Observation/           # Swift 5.9 Observation サポート
│   └── ComposableArchitectureMacros/  # Swift マクロ実装
├── Tests/                         # テストスイート
├── Examples/                      # サンプルアプリケーション
└── Documentation.docc/            # DocC ドキュメント

主要コンポーネント

  • Reducer Protocol: アプリケーションロジックを定義する中心的なプロトコル

    • 場所: Sources/ComposableArchitecture/Reducer.swift
    • 依存: State、Action、Effect
    • インターフェース: body: some Reducer<State, Action>
  • Store: アプリケーションのランタイム

    • 場所: Sources/ComposableArchitecture/Store.swift
    • 依存: Reducer、State
    • インターフェース: send(_:), scope(state:action:)
  • Effect: 副作用の表現と実行

    • 場所: Sources/ComposableArchitecture/Effect.swift
    • 依存: なし(独立した値型)
    • インターフェース: .none, .run, .send

技術スタック

コア技術

  • 言語: Swift 5.9+、最新のSwift機能(マクロ、Observation)を活用
  • フレームワーク: SwiftUI、UIKit、Combine
  • 主要ライブラリ:
    • swift-dependencies (1.4.0+): 依存性注入
    • swift-case-paths (1.5.4+): 列挙型の処理
    • swift-perception (1.3.4+): 観察機能のバックポート
    • swift-navigation (2.3.0+): ナビゲーション統合

開発・運用ツール

  • ビルドツール: Swift Package Manager
  • テスト: XCTest、カスタムTestStore
  • CI/CD: GitHub Actions
  • ドキュメント: DocC、GitHub Pages

設計パターン・手法

  • 単方向データフロー(Elm Architecture/Redux パターン)
  • 関数型プログラミング(純粋関数、不変性)
  • プロトコル指向プログラミング
  • 依存性注入
  • 型安全性の徹底活用

データフロー・処理フロー

  1. ユーザーインタラクションやシステムイベントがActionを生成
  2. StoreがActionを受け取り、現在のStateとともにReducerに渡す
  3. Reducerが新しいStateを計算し、必要な副作用(Effect)を返す
  4. Storeが新しいStateを保存し、ViewやViewStoreに通知
  5. 副作用が実行され、結果が新たなActionとしてStoreに送信

API・インターフェース

公開API

Store

  • 目的: アプリケーションの状態管理とアクション処理
  • 使用例:
let store = Store(initialState: Feature.State()) {
  Feature()
    ._printChanges()  // デバッグ用
}

// アクションの送信
store.send(.buttonTapped)

// 状態の観察(SwiftUI)
Text("\(store.count)")

設定・カスタマイズ

設定ファイル

// 依存性の登録
extension DependencyValues {
  var apiClient: APIClient {
    get { self[APIClient.self] }
    set { self[APIClient.self] = newValue }
  }
}

// テスト用のモック
store.dependencies.apiClient = .mock

拡張・プラグイン開発

カスタムReducer修飾子、依存性、Effectの作成が可能

パフォーマンス・スケーラビリティ

パフォーマンス特性

  • 状態の変更は同期的で予測可能
  • Observation統合により必要な部分のみ再レンダリング
  • IdentifiedArrayCollectionsによる効率的なコレクション操作

スケーラビリティ

モジュール化により、大規模アプリケーションでも管理可能。各機能を独立したモジュールとして開発し、後で統合

制限事項

  • 学習曲線が急(関数型プログラミングの概念)
  • ボイラープレートコードがやや多い(マクロで軽減)
  • Appleプラットフォーム専用

評価・所感

技術的評価

強み

  • 極めて高いテスタビリティ(時間ベースの操作も制御可能)
  • 予測可能で一貫性のあるアーキテクチャ
  • 優れたモジュール性と再利用性
  • 型安全性による実行時エラーの削減
  • 活発なコミュニティと継続的な改善

改善の余地

  • 初学者にとっての学習コストが高い
  • 小規模アプリケーションには過剰な場合がある
  • パフォーマンス最適化が必要な場合の複雑さ

向いている用途

  • 中〜大規模のiOS/macOSアプリケーション
  • 複雑な状態管理が必要なアプリ
  • チーム開発でアーキテクチャの統一が重要な場合
  • テストカバレッジを重視するプロジェクト

向いていない用途

  • プロトタイプや小規模アプリ
  • 関数型プログラミングに不慣れなチーム
  • パフォーマンスが最重要なリアルタイムアプリ

総評

The Composable Architectureは、Swift/iOS開発における最も洗練されたアーキテクチャフレームワークの1つです。関数型プログラミングの原則に基づいた設計により、予測可能で保守しやすく、テスト可能なアプリケーションの構築を可能にします。学習コストは高いものの、中〜大規模プロジェクトでは、その投資に見合う価値を提供します。Point-Freeチームによる継続的な改善と、活発なコミュニティのサポートにより、今後も進化し続けることが期待されます。