MENU

【SwiftUI】【TCA】無駄なViewの再描画をなくそう大作戦

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つの状態 (countotherState) を含めます。
  • subStateのプロパティ: AppStateの拡張としてsubStateプロパティを定義します。これにより、AppStateからSubStateに簡単にアクセスできます。
  • WithViewStoreのスコープ: WithViewStoreの中でstore.scope(state: \.subState)を使用し、SubStateに基づいてビューを更新します。

パターン2: observeを使う方法

このパターンでは、WithViewStoreobserve引数を使って特定の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()
            ))
        }
    }
}

解説

  • WithViewStoreobserve引数: WithViewStoreobserve引数を使って、特定のState(この場合はcountotherState)をタプルで渡します。これにより、これらのStateに変更があった場合のみビューが再描画されます。
  • viewStore.stateの使用: viewStore.stateは、observeで指定したタプルの形式で状態を持ちます。この例では、viewStore.state.0countviewStore.state.1otherStateを表しています

どちらのパターンを採用するかの判断基準

パターン1: subStateを使う方法

利点

  1. 明確な状態の分離: 使用する状態がサブステートとして明確に分離されるため、コードの可読性が向上します。
  2. 再利用性の向上: サブステートを他のビューやコンポーネントでも再利用しやすくなります。
  3. 型安全性: スコープ内で型安全に状態を扱うことができます。

欠点

  1. 初期設定が必要: サブステートを定義し、メインの状態からそのサブステートを作成する必要があります。
  2. 複雑な状態管理: サブステートが複雑になる場合、管理が難しくなることがあります。

適用シナリオ

  • 特定のビューやコンポーネントが複数の状態を使用する場合。
  • サブステートが他のビューやコンポーネントでも再利用される場合。
  • 状態の明確な分離が必要な場合。

パターン2: observeを使う方法

利点

  1. 簡潔なコード: observe引数を使うことで、必要な状態のみを監視するためのコードが簡潔になります。
  2. 迅速な実装: 追加の構造体やプロパティを定義する必要がなく、迅速に実装できます。

欠点

  1. 型安全性の欠如: タプルを使用するため、状態に対して型安全な操作がしにくくなります。
  2. 可読性の低下: タプルを多用することで、コードの可読性が低下する可能性があります。

適用シナリオ

  • 迅速に実装したい場合。
  • 単純な状態監視が必要な場合。
  • 追加の構造体を定義するほど状態が複雑でない場合。

具体的な判断基準

  1. コードの可読性: 長期的にメンテナンスしやすいコードを目指すなら、subStateを使う方法が適しています。明確な状態分離により、コードの理解が容易になります。
  2. 実装の簡潔さ: 迅速に実装を進めたい場合や、監視する状態が少ない場合は、observeを使う方法が適しています。追加の構造体を定義する手間を省けます。
  3. 再利用性: 同じ状態を複数のビューやコンポーネントで使用する予定がある場合は、subStateを使う方法が適しています。再利用性が高まり、コードの重複を避けることができます。
  4. 状態の複雑さ: 状態が複雑で、複数の部分に分割して管理する必要がある場合は、subStateを使う方法が適しています。サブステートを使うことで、状態管理がシンプルになり、バグの発生を防ぎやすくなります。

結論

どちらのパターンを採用するかは、具体的な状況やプロジェクトの要件に応じて決定すると良いでしょう。プロジェクトの規模や将来的な拡張性、チームのコーディングスタイルなども考慮しながら、最適な方法を選択してください。

さいごに

いかがでしたでしょうか?TCAは状態管理のアーキテクチャゆえに、何も考えずに実装してしまうと余計な処理が走ってしまい、処理の遅いアプリになってしまいます。
この記事で解説したことを理解すると、よりパフォーマンスの高いアプリを構築できるでしょう!
また、この記事を書くきっかけを作っていただいた私のメンティーさんのIさんとNさん、大変感謝ですm(__)m

年収4桁万円を達成中のiOSエンジニアが皆さんをお導きいたします!
ぜひメンターを受けてみてください〜
メンターはこちら
貴方もなれます!!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

Rio@iOSエンジニアのアバター Rio@iOSエンジニア 経営者兼モバイルアプリエンジニア

都内のモバイルアプリ開発会社経営者。
モバイルアプリの新規の請負開発及び保守運用を引き受ける。
Denso→Honda→現在
#RxSwift #MVVM #Firebase #Python3

コメント

コメントする

目次