FlutterエンジニアのためのSwiftUI & TCA入門 Part2
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が用意されています。
-
LazyHStack
とLazyVStack
: 必要に応じて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は、単方向データフローに基づいたアーキテクチャです。これは、状態の変化が一方向のみに流れることを意味し、状態の変化を追跡しやすくなります。単方向データフローは、複雑なアプリケーションの状態管理をシンプルにするための重要な概念です。
-
View
→Action
: ユーザーの操作はAction
として表現されます。 -
Action
→Reducer
:Action
はReducer
に渡され、状態の変化を計算します。 -
Reducer
→State
:Reducer
は、新しい状態を生成します。 -
State
→View
: 新しい状態はView
に反映され、UIが更新されます。
このサイクルを繰り返すことで、状態の変化を予測可能にし、デバッグを容易にします。
TCAの各要素の詳細
-
State
: アプリケーションの状態を保持する構造体。- 状態は、
Equatable
プロトコルに準拠する必要があります。 -
State
は、イミュータブル(不変)であるべきです。つまり、State
は変更するのではなく、新しいState
を生成する必要があります。
- 状態は、
-
Action
: ユーザーインタラクションやシステムイベントを表現するenum。-
Action
は、具体的なイベントを表す必要があります。 -
Action
は、副作用を引き起こす可能性があります。
-
-
Environment
: 副作用(API呼び出し、データベースアクセスなど)を処理する依存関係を保持する構造体。-
Environment
は、テスト容易性を高めるために、依存関係を抽象化します。 -
Environment
は、非同期処理を行う関数を保持します。
-
-
Reducer
:State
とAction
を受け取り、新しいState
と副作用を返す関数。-
Reducer
は、純粋関数である必要があります。つまり、同じState
とAction
を与えれば、常に同じ結果を返す必要があります。 -
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
を使って、Action
をStore
に送信します。
中規模〜大規模アプリでの実践
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のEnvironment
とEffect
の概念を理解する上で役立ちます。副作用を明確に分離し、テスト可能なコードを書くことができます。
まとめ
このドキュメントでは、FlutterエンジニアがSwiftUIとTCAを使った中規模〜大規模アプリ開発を始めるための実践的なガイドを提供しました。SwiftUIの深い理解とTCAのアーキテクチャの習得は、より複雑なiOSアプリ開発を成功させるための鍵となります。Flutterでの経験を活かし、Swiftの世界でも素晴らしいアプリを開発してください。
このガイドが、あなたのSwiftUIとTCAの学習の一助となることを願っています。
Discussion