SwiftUIとTCAで一部のStateの変更によってViewが更新される方法
SwiftUIとThe Composable Architecture (TCA)で、一部のStateの変更によってViewが更新されるようにする方法には、以下の2つのパターンがあります。それぞれのパターンについて、コード例とともに解説します。また、どちらのパターンを採用するかの判断基準についても説明します。
目次
パターン1: subState
を使う方法
このパターンでは、使用したい2つのStateをまとめたサブステートを作成し、それをWithViewStore
で使用します。
Stateの定義
import SwiftUI
import ComposableArchitecture
struct AppState: Equatable {
var count: Int = 0
var otherState: String = ""
var anotherState: Bool = false
}
enum AppAction: Equatable {
case increment
case decrement
case updateOtherState(String)
case toggleAnotherState
}
struct AppEnvironment {
}
struct AppReducer: ReducerProtocol {
struct State: Equatable {
var count: Int = 0
var otherState: String = ""
var anotherState: Bool = false
}
enum Action: Equatable {
case increment
case decrement
case updateOtherState(String)
case toggleAnotherState
}
typealias Environment = AppEnvironment
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .increment:
state.count += 1
return .none
case .decrement:
state.count -= 1
return .none
case let .updateOtherState(value):
state.otherState = value
return .none
case .toggleAnotherState:
state.anotherState.toggle()
return .none
}
}
}
}
サブステートの定義
struct SubState: Equatable {
var count: Int
var otherState: String
}
Viewの定義
struct ContentView: View {
let store: StoreOf<AppReducer>
var body: some View {
WithViewStore(self.store.scope(state: \.subState)) { viewStore in
VStack {
Text("Count: \(viewStore.count)")
Text("Other State: \(viewStore.otherState)")
HStack {
Button("-") {
viewStore.send(.decrement)
}
Button("+") {
viewStore.send(.increment)
}
}
Button("Update Other State") {
viewStore.send(.updateOtherState("New Value"))
}
Button("Toggle Another State") {
viewStore.send(.toggleAnotherState)
}
}
}
}
}
private extension AppState {
var subState: SubState {
SubState(count: self.count, otherState: self.otherState)
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView(store: Store(
initialState: AppReducer.State(),
reducer: AppReducer(),
environment: AppEnvironment()
))
}
}
}
解説
SubState
の定義:SubState
構造体を作成し、必要な2つの状態 (count
とotherState
) を含めます。subState
のプロパティ:AppState
の拡張としてsubState
プロパティを定義します。これにより、AppState
からSubState
に簡単にアクセスできます。WithViewStore
のスコープ:WithViewStore
の中でstore.scope(state: \.subState)
を使用し、SubState
に基づいてビューを更新します。
パターン2: observe
を使う方法
このパターンでは、WithViewStore
のobserve
引数を使って特定のStateの変更に対してのみビューを再描画します。
Stateの定義
import SwiftUI
import ComposableArchitecture
struct AppState: Equatable {
var count: Int = 0
var otherState: String = ""
var anotherState: Bool = false
}
enum AppAction: Equatable {
case increment
case decrement
case updateOtherState(String)
case toggleAnotherState
}
struct AppEnvironment {
}
struct AppReducer: ReducerProtocol {
struct State: Equatable {
var count: Int = 0
var otherState: String = ""
var anotherState: Bool = false
}
enum Action: Equatable {
case increment
case decrement
case updateOtherState(String)
case toggleAnotherState
}
typealias Environment = AppEnvironment
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .increment:
state.count += 1
return .none
case .decrement:
state.count -= 1
return .none
case let .updateOtherState(value):
state.otherState = value
return .none
case .toggleAnotherState:
state.anotherState.toggle()
return .none
}
}
}
}
Viewの定義
struct ContentView: View {
let store: StoreOf<AppReducer>
var body: some View {
WithViewStore(self.store, observe: { ($0.count, $0.otherState) }) { viewStore in
VStack {
Text("Count: \(viewStore.state.0)")
Text("Other State: \(viewStore.state.1)")
HStack {
Button("-") {
viewStore.send(.decrement)
}
Button("+") {
viewStore.send(.increment)
}
}
Button("Update Other State") {
viewStore.send(.updateOtherState("New Value"))
}
Button("Toggle Another State") {
viewStore.send(.toggleAnotherState)
}
}
}
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView(store: Store(
initialState: AppReducer.State(),
reducer: AppReducer(),
environment: AppEnvironment()
))
}
}
}
解説
WithViewStore
のobserve
引数:WithViewStore
のobserve
引数を使って、特定のState(この場合はcount
とotherState
)をタプルで渡します。これにより、これらのStateに変更があった場合のみビューが再描画されます。
viewStore.state
の使用:viewStore.state
は、observe
で指定したタプルの形式で状態を持ちます。この例では、viewStore.state.0
がcount
、viewStore.state.1
がotherState
を表しています
どちらのパターンを採用するかの判断基準
パターン1: subState
を使う方法
利点
- 明確な状態の分離: 使用する状態がサブステートとして明確に分離されるため、コードの可読性が向上します。
- 再利用性の向上: サブステートを他のビューやコンポーネントでも再利用しやすくなります。
- 型安全性: スコープ内で型安全に状態を扱うことができます。
欠点
- 初期設定が必要: サブステートを定義し、メインの状態からそのサブステートを作成する必要があります。
- 複雑な状態管理: サブステートが複雑になる場合、管理が難しくなることがあります。
適用シナリオ
- 特定のビューやコンポーネントが複数の状態を使用する場合。
- サブステートが他のビューやコンポーネントでも再利用される場合。
- 状態の明確な分離が必要な場合。
パターン2: observe
を使う方法
利点
- 簡潔なコード:
observe
引数を使うことで、必要な状態のみを監視するためのコードが簡潔になります。 - 迅速な実装: 追加の構造体やプロパティを定義する必要がなく、迅速に実装できます。
欠点
- 型安全性の欠如: タプルを使用するため、状態に対して型安全な操作がしにくくなります。
- 可読性の低下: タプルを多用することで、コードの可読性が低下する可能性があります。
適用シナリオ
- 迅速に実装したい場合。
- 単純な状態監視が必要な場合。
- 追加の構造体を定義するほど状態が複雑でない場合。
具体的な判断基準
- コードの可読性: 長期的にメンテナンスしやすいコードを目指すなら、
subState
を使う方法が適しています。明確な状態分離により、コードの理解が容易になります。 - 実装の簡潔さ: 迅速に実装を進めたい場合や、監視する状態が少ない場合は、
observe
を使う方法が適しています。追加の構造体を定義する手間を省けます。 - 再利用性: 同じ状態を複数のビューやコンポーネントで使用する予定がある場合は、
subState
を使う方法が適しています。再利用性が高まり、コードの重複を避けることができます。 - 状態の複雑さ: 状態が複雑で、複数の部分に分割して管理する必要がある場合は、
subState
を使う方法が適しています。サブステートを使うことで、状態管理がシンプルになり、バグの発生を防ぎやすくなります。
結論
どちらのパターンを採用するかは、具体的な状況やプロジェクトの要件に応じて決定すると良いでしょう。プロジェクトの規模や将来的な拡張性、チームのコーディングスタイルなども考慮しながら、最適な方法を選択してください。
さいごに
いかがでしたでしょうか?TCAは状態管理のアーキテクチャゆえに、何も考えずに実装してしまうと余計な処理が走ってしまい、処理の遅いアプリになってしまいます。
この記事で解説したことを理解すると、よりパフォーマンスの高いアプリを構築できるでしょう!
また、この記事を書くきっかけを作っていただいた私のメンティーさんのIさんとNさん、大変感謝ですm(__)m
年収4桁万円を達成中のiOSエンジニアが皆さんをお導きいたします!
ぜひメンターを受けてみてください〜
メンターはこちら
貴方もなれます!!
コメント