TCAで2つのコンポーネントで共有の値を使う
概要
TCAで複数のコンポーネントで共有の値を持つための公式サンプルがあったので書いておきます。サンプル名は「Shared State Demo」で、Counter画面とProfile画面があってCounter画面で増減した値がProfile画面で表示されています。
Shared State Demo
共有するStateはどこにあるか
SharedState
というStateがRootのStateととなっており、その子としてCounterState
とprofileState
を持ちます。
省略したStateを示します。
struct SharedState: Equatable {
var counter = CounterState()
var profile: ProfileState { ... }
// タブ切り替えActionで切り替えられる
var currentTab = Tab.counter
...
}
- SharedState
- CounterState
- カウンターを増減(こいつがソース)
- ProfileState
- 増減された情報を表示
- リセットもできる
- CounterState
(共有したいStateはCounterState
にあるわけで、SharedState
にもたせているわけではありません。基本的に共通のStateを持つ場合にそれら共有したい子要素の親が共有されるStateを持たせたいと思いますが、このサンプルはそうなっていません)
共有するStateはどのタイミングで共有されるのか
タイミングはSwitchをタップされProfile画面が表示されるときです。その際にProfile画面用のStateがCounterState
から作成されます。
上記で省略していたSharedState
についてもう少し詳しく書きます。
struct SharedState: Equatable {
var counter = CounterState()
var currentTab = Tab.counter
struct CounterState: Equatable {
var alert: AlertState<SharedStateAction.CounterAction>?
// 増減するcount
var count = 0
var maxCount = 0
var minCount = 0
var numberOfCounts = 0
}
var profile: ProfileState {
get { // ここでProfileStateが取得される際にcounterから作成される
ProfileState(
currentTab: self.currentTab,
count: self.counter.count,
maxCount: self.counter.maxCount,
minCount: self.counter.minCount,
numberOfCounts: self.counter.numberOfCounts
)
}
set {
self.currentTab = newValue.currentTab
self.counter.count = newValue.count
self.counter.maxCount = newValue.maxCount
self.counter.minCount = newValue.minCount
self.counter.numberOfCounts = newValue.numberOfCounts
}
}
...
ProfileStateが取得されるとは?
SwichがタップされたときにProfile画面を出そうとする際にSharedStateProfileView
表示しようとしてstore.scope
でprofileStateを取得します(getが動作する)。
struct SharedStateView: View {
let store: Store<SharedState, SharedStateAction>
var body: some View {
WithViewStore(self.store.scope(state: \.currentTab)) { viewStore in
VStack {
Picker(
"Tab",
selection: viewStore.binding(send: SharedStateAction.selectTab)
) {
Text("Counter")
.tag(SharedState.Tab.counter)
Text("Profile")
.tag(SharedState.Tab.profile)
}
.pickerStyle(SegmentedPickerStyle())
if viewStore.state == .counter {
SharedStateCounterView(
store: self.store.scope(state: \.counter, action: SharedStateAction.counter))
}
if viewStore.state == .profile {
SharedStateProfileView(
store: self.store.scope(state: \.profile, action: SharedStateAction.profile))
}
Spacer()
}
}
.padding()
}
}
self.store.scope(state: \.profile, action: SharedStateAction.profile))
の部分で\.profile
でKeyPathとしてStateからprofileへアクセスしています。
KeyPathについての蛇足: SE-0249: Key Path Expressions as Functions
これは「SE-0249: Key Path Expressions as Functions」でしょう。KeyPathで式書いたらそれがクロージャに解釈されるよってな感じです。
- KeyPathで
\.profile
って式を書いて関数に渡す-
{ $0.profile }
クロージャが関数に渡されててここでgetされる-
$0
はSharedState
-
-
まとめ
- TCAのShared State DemoはデータをCounterとProfileの2画面で共有します
- データ自体は
CounterState
にあります - Profile画面は表示時に
ProfileState
がCounterState
から作られます- プロパティのget時に
ProfileState
を作っています- getが動作するのは画面表示の際に
store.scope(state: \.profile, action: SharedStateAction.profile))
が実行されるためです
- getが動作するのは画面表示の際に
- プロパティのget時に
- データ自体は
これはあくまでサンプルのやり方であって違うやり方もできるでしょう。他にはaccessTokenなどの副作用限定のグローバル変数の扱いはもっと良いやり方がありますし、RootのStateに共有のStateをもたせることもありでしょう。
ちなみにProfile画面でresetする際に元データをリセットしてるのはsend(アクション)時にプロパティのsetが動くから。Reducerの関数が動作するとStateが書き換わるのを利用してそう。
Discussion