LoginSignup
25
11

More than 3 years have passed since last update.

SwiftUIで単方向データフローどうなるか少し触ってみた

Last updated at Posted at 2019-06-05

SwiftUIがWWDCで発表されましたね、
FlutterやJetpack ComponentとかからコードでUIを書く流れが公式系でも
提供されるようになったなぁと思っていたところでAppleからも公式の物が発表される流れになりましたね。

単方向データフローやるならどうやるんだろう

WWDCのセッションでもデータフローまわりのセッションがありそうですが、
ちょっと自分でも手探りでやってみました。

だいたいこんな流れ?

swift-ui-redux(1).png

Action

Actionはまぁ通常通りenumで定義しておきます

protocol Action {}
enum CounterAction: Action {
    case CountUp
    case CountDown
}

State

//Stateという名前にするとSwiftUIのStateと衝突するので、ReduxSwiftという名前に一旦しておいた
protocol ReduxState {}

struct CounterState: ReduxState {
    let count: Int
}

struct AppState: ReduxState {
    private(set) var counterState: CounterState = CounterState(count: 0)
}

Reducer

protocol Reducer {
    associatedtype StateType: ReduxState
    func reduce(s: StateType, a: Action) -> StateType
}

final class CounterStateReducer: Reducer {
    typealias StateType = CounterState

    func reduce(s: CounterState, a: Action) -> CounterState {
        switch(a) {
        case CounterAction.CountDown:
            return CounterState(count: s.count - 1)
        case CounterAction.CountUp:
            return CounterState(count: s.count + 1)
        default:
            return s
        }
    }
}

final class AppStateReducer: Reducer {
    typealias StateType = AppState

    func reduce(s: AppState, a: Action) -> AppState {
        return AppState(
            counterState: CounterStateReducer().reduce(s: s.counterState, a: a)
        )
    }
}

BindableObjectをStoreとして使う

BindableObjectをEnvironmentObjectとして使う事で
木構造の下に自動的に伝搬する機能と、変更を通知する機能を提供できるので
そのようにいったんしてみました。

final class AppData: BindableObject {
    var didChange = PassthroughSubject<AppState, Never>()

    private (set) var appState = AppState()

    func dispatch(a: Action) {
        self.appState = AppStateReducer().reduce(s: appState, a: a)
        didChange.send(self.appState)
    }
}

Viewから使う

import SwiftUI

struct ParentView: View {
    var body: some View {
        NavigationView {
            VStack {
                Text("Parent: \(UUID().uuidString)")
                ContentView()
            }
        }
    }
}

struct CounterView: View {
    @EnvironmentObject var appData: AppData

    var body: some View {
        List {
            Text("CounterView: \(UUID().uuidString)")
            Text("Counter: \(self.appData.appState.counterState.count)")
        }
    }
}

struct CounterButton: View {
    @EnvironmentObject var appData: AppData

    var body: some View {
        List {
            Text("CounterButton: \(UUID().uuidString)")
            Button(action: {
                self.appData.dispatch(a: CounterAction.CountUp)
            }) {
                Text("Count Up")
            }
            Button(action: {
                self.appData.dispatch(a: CounterAction.CountDown)
            }) {
                Text("Count Down")
            }
        }
    }
}

struct ContentView : View {
    @EnvironmentObject var appData: AppData

    var body: some View {
        VStack {
            Text("ContentView: \(UUID().uuidString)")
            Text("CounterView ↓")
            CounterView()
            Text("CounterButton ↓")
            CounterButton()
        }
    }
}

Viewからは、@EnvironmentObjectを通してAppDataを参照できるので、
ActionをDispatchしたり、変更があった際に表示を更新したりできます。
実行してみるとCounterButtonのボタンを押すと、CounterViewのカウント表示が更新されるのがわかります。

ところどころにUUIDを付与してみているのは、再描画がどのレイヤーでどのタイミングで行われるか確認したかったためで、
ParentViewはこの場合は再描画されない事がわかります。
どうも@EnvironmentObjectを使っているかどうかという所でわかれそうで、たとえbodyの中で参照していなくても
@EnvironmentObjectで参照するようにすると再描画されるようになります。

コード

今回書いてみたコードは以下のリポジトリに上げてみています。
https://github.com/pocket7878/swift-ui-redux-like

まだまだ情報がすくなくてベストプラクティスがわかりませんが、これから色々拡充されていくのが楽しみですね。

25
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
11