🦜

FlutterエンジニアのためのSwiftUI & TCA入門 Part2

2025/01/18に公開

FlutterエンジニアのためのSwiftUI & TCA入門 Part2: 中規模〜大規模アプリ改修を見据えて

はじめに

※自分の学習用メモとしての意味合いが強いです🚶

Part2となる今回は、単なる入門編ではなく、中規模から大規模なアプリ開発において、SwiftUIとTCAを最大限に活用するための実践的なガイドです。

このドキュメントでは、SwiftUIの基本概念をより深く掘り下げ、TCAのアーキテクチャを詳細に解説します。また、Flutterでの経験をどのようにSwiftの世界で活かすか、具体的なコード例や実践的なノウハウを交えながら説明します。特に、中規模から大規模アプリの改修において直面するであろう課題に焦点を当て、それらを解決するための戦略とテクニックを提示します。

SwiftUIの深層:基本から応用へ

SwiftUIのアーキテクチャとレンダリング

SwiftUIは、宣言的UIフレームワークであり、UIの構造と状態の変化に応じて自動的にUIを更新します。この自動更新の仕組みを理解することは、複雑なUIを効率的に開発する上で非常に重要です。

  • Viewツリー: SwiftUIは、Viewを組み合わせてUIを構築します。これらのViewは、階層的なViewツリーとして管理されます。
  • 状態の変化: @State, @Binding, @ObservedObject, @EnvironmentObjectなどのプロパティラッパーは、Viewの状態を保持し、状態が変化すると、関連するViewが再レンダリングされます。
  • レンダリングプロセス: SwiftUIは、状態の変化を検知し、必要なViewのみを再レンダリングすることで、パフォーマンスを最適化します。
  • KeyPath: SwiftUIは、Viewを識別するために、内部的にKeyPathを使用します。KeyPathは、Viewの構造や状態を特定するために用いられます。

Viewのライフサイクル

SwiftUIのViewには、ライフサイクルが存在します。ライフサイクルを理解することで、Viewの初期化や破棄処理を適切に行うことができます。

  • init(): Viewが最初に初期化される際に呼ばれます。
  • onAppear(perform:): Viewが画面に表示される直前に呼ばれます。
  • onDisappear(perform:): Viewが画面から消える直前に呼ばれます。
  • onChange(of:perform:): 指定した状態が変化した際に呼ばれます。

これらのライフサイクルメソッドを活用することで、Viewの表示/非表示時に必要な処理(データの取得、タイマーの開始/停止など)を制御できます。

より高度なレイアウト

SwiftUIでは、HStack, VStack, ZStackに加えて、より複雑なレイアウトを実現するための様々なコンテナViewが用意されています。

  • LazyHStackLazyVStack: 必要に応じてViewをレンダリングするLazyStackです。大量のデータを扱う場合にパフォーマンスを向上させることができます。
  • ScrollView: スクロール可能なViewを作成します。
  • List: リスト形式のViewを作成します。
  • Grid: グリッド形式のViewを作成します。

これらのViewを組み合わせることで、複雑なUIレイアウトを効率的に実現できます。また、GeometryReaderを使用して、Viewのサイズや位置を動的に計算することも可能です。

Animation

SwiftUIでは、withAnimationを使ってアニメーションを簡単に追加できます。

@State private var isExpanded = false

var body: some View {
    VStack {
        Button("Toggle") {
            withAnimation {
                isExpanded.toggle()
            }
        }

        if isExpanded {
            Text("Expanded Content")
                .transition(.scale)
        }
    }
}

withAnimationブロック内で状態を変更すると、SwiftUIが自動的にアニメーションを追加します。transitionを使って、Viewの表示/非表示のアニメーションをカスタマイズすることもできます。

カスタムViewの作成と再利用

SwiftUIでは、Viewプロトコルに準拠した構造体やクラスを作成することで、カスタムViewを作成できます。カスタムViewを作成することで、UIの再利用性を高めることができます。

struct CustomButton: View {
    let title: String
    let action: () -> Void

    var body: some View {
        Button(action: action) {
            Text(title)
                .padding()
                .background(.blue)
                .foregroundColor(.white)
                .cornerRadius(10)
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            CustomButton(title: "Tap Me") {
                print("Button tapped!")
            }
        }
    }
}

CustomButtonのように、再利用可能なViewを作成することで、コードの重複を減らし、UIの一貫性を保つことができます。

TCA(The Composable Architecture)の徹底解剖

単方向データフローの重要性

TCAは、単方向データフローに基づいたアーキテクチャです。これは、状態の変化が一方向のみに流れることを意味し、状態の変化を追跡しやすくなります。単方向データフローは、複雑なアプリケーションの状態管理をシンプルにするための重要な概念です。

  1. ViewAction: ユーザーの操作はActionとして表現されます。
  2. ActionReducer: ActionReducerに渡され、状態の変化を計算します。
  3. ReducerState: Reducerは、新しい状態を生成します。
  4. StateView: 新しい状態はViewに反映され、UIが更新されます。

このサイクルを繰り返すことで、状態の変化を予測可能にし、デバッグを容易にします。

TCAの各要素の詳細

  • State: アプリケーションの状態を保持する構造体。
    • 状態は、Equatableプロトコルに準拠する必要があります。
    • Stateは、イミュータブル(不変)であるべきです。つまり、Stateは変更するのではなく、新しいStateを生成する必要があります。
  • Action: ユーザーインタラクションやシステムイベントを表現するenum。
    • Actionは、具体的なイベントを表す必要があります。
    • Actionは、副作用を引き起こす可能性があります。
  • Environment: 副作用(API呼び出し、データベースアクセスなど)を処理する依存関係を保持する構造体。
    • Environmentは、テスト容易性を高めるために、依存関係を抽象化します。
    • Environmentは、非同期処理を行う関数を保持します。
  • Reducer: StateActionを受け取り、新しいStateと副作用を返す関数。
    • Reducerは、純粋関数である必要があります。つまり、同じStateActionを与えれば、常に同じ結果を返す必要があります。
    • Reducerは、副作用を表すEffectを返します。
  • Store: State, Action, Environment, Reducerを保持し、状態の変化を管理するクラス。
    • Storeは、Stateを監視し、変化をViewに通知します。
    • Storeは、副作用を実行します。

Reducerの型と副作用の扱い方

TCAのReducerは、(inout State, Action, Environment) -> Effect<Action>という型を持ちます。これは、現在のState, Action, Environmentを受け取り、新しいStateと副作用を返すことを意味します。

let myReducer = Reducer<MyState, MyAction, MyEnvironment> { state, action, environment in
    switch action {
    case .loadData:
        // 副作用を実行し、データを取得する
        return environment.loadData()
            .map(MyAction.dataLoaded)
            .eraseToEffect()

    case .dataLoaded(let data):
        // データをStateに格納する
        state.data = data
        return .none
    }
}
  • Effect: 副作用を表す型です。Effectは、非同期処理の結果をActionに変換するために使用されます。
  • map: 副作用の結果をActionに変換します。
  • eraseToEffect: Effectを型消去するために使用します。
  • .none: 副作用がない場合に返します。

環境(Environment)の重要性

TCAのEnvironmentは、副作用を抽象化し、テスト容易性を高めるための重要な要素です。Environmentは、APIクライアント、データベースアクセスオブジェクト、タイマーなどを保持します。

struct MyEnvironment {
    var loadData: () -> Effect<String>
    var saveSettings: (Settings) -> Effect<Void>
}

Environmentに依存関係を注入することで、Reducerはテスト可能な状態に保たれます。

Storeの活用

Storeは、状態の変化を管理し、Viewに状態を反映します。Storeは、WithViewStoreを使って、Viewに状態をバインドします。

struct MyView: View {
  let store: Store<MyState, MyAction>

  var body: some View {
    WithViewStore(store) { viewStore in
      Text("Count: \(viewStore.count)")
      Button("Increment") {
        viewStore.send(.increment)
      }
    }
  }
}

WithViewStoreは、Storeの状態を監視し、状態が変化すると、Viewを再レンダリングします。viewStore.sendを使って、ActionStoreに送信します。

中規模〜大規模アプリでの実践

Feature Module化

大規模アプリでは、機能をモジュール化することが重要です。TCAでは、各機能を独立したモジュールとして作成することができます。

// Feature 1
struct Feature1State: Equatable { ... }
enum Feature1Action { ... }
struct Feature1Environment { ... }
let feature1Reducer = Reducer<Feature1State, Feature1Action, Feature1Environment> { ... }

// Feature 2
struct Feature2State: Equatable { ... }
enum Feature2Action { ... }
struct Feature2Environment { ... }
let feature2Reducer = Reducer<Feature2State, Feature2Action, Feature2Environment> { ... }

各機能は、State, Action, Environment, Reducerを持つ独立したモジュールとして作成されます。

ComposableなReducerの実現

TCAでは、複数のReducerを組み合わせて、より複雑なReducerを作成することができます。

let rootReducer = Reducer<RootState, RootAction, RootEnvironment> { state, action, environment in
    switch action {
        case .feature1Action(let feature1Action):
            return feature1Reducer.run(&state.feature1State, feature1Action, environment.feature1Environment)
        case .feature2Action(let feature2Action):
            return feature2Reducer.run(&state.feature2State, feature2Action, environment.feature2Environment)
        ...
    }
}

runメソッドを使うことで、親Reducerから子Reducerを実行することができます。これにより、複雑な状態管理をよりシンプルに行うことができます。

エラーハンドリング

TCAでは、Reducer内でエラーをハンドリングすることができます。

let myReducer = Reducer<MyState, MyAction, MyEnvironment> { state, action, environment in
    switch action {
    case .loadData:
      return environment.loadData()
        .map(MyAction.dataLoaded)
        .catch { error in
          return Effect(value: .dataLoadFailed(error))
        }
        .eraseToEffect()
      case .dataLoaded(let data):
          state.data = data
          return .none
      case .dataLoadFailed(let error):
         state.error = error
         return .none
    }
}

catchメソッドを使うことで、エラーをキャッチし、エラーをActionとしてReducerに送信することができます。

テストの重要性

TCAは、テスト容易性が高いアーキテクチャです。各コンポーネントが独立しているため、ユニットテストを容易に書くことができます。

import XCTest
import ComposableArchitecture

func testReducer(){
    let reducer = Reducer<CounterState, CounterAction, CounterEnvironment> { state, action, _ in
        switch action {
        case .incrementButtonTapped:
            state.count += 1
            return .none
        case .decrementButtonTapped:
            state.count -= 1
            return .none
        }
    }

  var state = CounterState()
  let store = TestStore(
    initialState: state,
    reducer: reducer,
    environment: CounterEnvironment()
  )

  store.send(.incrementButtonTapped) {
      $0.count = 1
  }
  store.send(.decrementButtonTapped) {
      $0.count = 0
  }
}

TestStoreを使うことで、Reducerの動作を簡単にテストすることができます。

Flutterの知識をSwiftUI/TCAで活かす(再考)

宣言的UIの原則

Flutterの宣言的UIの原則は、SwiftUIでも同様に適用できます。UIをどのように表示するかを記述することで、SwiftUIが自動的にUIをレンダリングします。

状態管理の経験

FlutterでRiverpodやflutter_hooksを使って状態管理をしていた経験は、TCAの単方向データフローの理解を助けます。状態、アクション、副作用を明確に分離することで、複雑な状態管理をシンプルにできます。

副作用管理の経験

Flutterでの非同期処理や副作用の管理経験は、TCAのEnvironmentEffectの概念を理解する上で役立ちます。副作用を明確に分離し、テスト可能なコードを書くことができます。

まとめ

このドキュメントでは、FlutterエンジニアがSwiftUIとTCAを使った中規模〜大規模アプリ開発を始めるための実践的なガイドを提供しました。SwiftUIの深い理解とTCAのアーキテクチャの習得は、より複雑なiOSアプリ開発を成功させるための鍵となります。Flutterでの経験を活かし、Swiftの世界でも素晴らしいアプリを開発してください。

このガイドが、あなたのSwiftUIとTCAの学習の一助となることを願っています。

Discussion