SwiftUIがWWDCで発表されましたね、
FlutterやJetpack ComponentとかからコードでUIを書く流れが公式系でも
提供されるようになったなぁと思っていたところでAppleからも公式の物が発表される流れになりましたね。
単方向データフローやるならどうやるんだろう
WWDCのセッションでもデータフローまわりのセッションがありそうですが、
ちょっと自分でも手探りでやってみました。
だいたいこんな流れ?
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
まだまだ情報がすくなくてベストプラクティスがわかりませんが、これから色々拡充されていくのが楽しみですね。