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

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

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

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

https://developer.apple.com/videos/play/wwdc2019/226/

# だいたいこんな流れ?

![swift-ui-redux(1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14359/b02b4055-c75a-5492-c6e9-f02834e997ab.png)


## Action

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

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

## State

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

struct CounterState: ReduxState {
    let count: Int
}

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

## Reducer

```swift
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として使う事で
木構造の下に自動的に伝搬する機能と、変更を通知する機能を提供できるので
そのようにいったんしてみました。

```swift
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から使う

```swift
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

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