📐
TCAでViewStateとViewActionを利用する
はじめに
The Composable Architcture(TCA)のサンプルではViewStateとViewActionをもとのStateとActionから分離している例があります。それについて小さなコードで説明してみます。
ViewStateやViewActionとは何か
ViewState
- 実装方法
- Stateに対してComputed propertyをextensionで増やす
- 用途
- View特有の変換をしたい場合などに使える
- ViewHelper的な使い方
- View特有の変換をしたい場合などに使える
ViewAction
- 実装方法
- ViewActionからActionへ変換する関数を用意する
- 用途
- ViewActionから渡される値を変更したい
- 例: 値がnilの場合にカラ文字に置き換える
- ViewActionから渡される値を変更したい
- できないこと
- Stateを変更することはできない
- ViewActionがあるからといって、もとのActionの呼び出しを封じたりはできない
- ActionはViewActionからも呼び出せるし、Viewからも呼び出せる
大雑把なイメージ
以降出てくるサンプルコードと連携していない抽象的なイメージとしては次のとおりです
実装例
-ボタンと+ボタンのあるカウンター画面を実装してみる。
仕様
- -ボタンで-1
- +ボタンで+1
- 3桁以上ならカンマ区切り
- 例:
1,000
- 例:
- 0以上なら
- 「0以上だよ!」表示
- 背景色を緑に
- 0未満なら
- 「マイナスだよ!」表示
- 背景色を赤に
スクリーンショット
設計方針
- TCAで必須ではないがenumで
ContentCore
として名前空間的にまとめておく- 例:
ContentCore.State
やContentCore.Action
とし、ContentAction
やContentAction
にはしない
- 例:
ソースコード
import SwiftUI
import ComposableArchitecture
enum ContentCore {
struct State: Equatable {
var count = 1000
}
enum Action {
case onAppear
case increment
case decrement
}
struct Environment { }
static let reducer = Reducer<State, Action, Environment> { state, action, _ in
switch action {
case .onAppear:
return .none
case .decrement:
state.count -= 1
return .none
case .increment:
state.count += 1
return .none
}
}
struct View: SwiftUI.View {
// MARK: -
struct State: Equatable {
var count: String
var description: String
var backgroundColor: Color
}
enum Action {
case increment
case decrement
}
// MARK: -
let store: Store<ContentCore.State, ContentCore.Action>
var body: some SwiftUI.View {
WithViewStore(store) { _ in
// ViewStateとViewActionを取り出す
WithViewStore(
store.scope(
state: { $0.view },
action: ContentCore.Action.view
)
) { viewStore in
VStack {
Text("\(viewStore.count)")
Text("\(viewStore.description)")
.background(viewStore.backgroundColor)
.padding()
HStack {
Button("-") {
viewStore.send(.decrement)
}
Button("+") {
viewStore.send(.increment)
}
}
}
}
}
}
}
}
// MARK: - ViewState
extension ContentCore.State {
// Stateに対してプロパティを増やしている。
// Stateに対してView特有の変換をしたい場合などのViewHelperみたいな用途に使える。
var view: ContentCore.View.State {
.init(
count: String.localizedStringWithFormat("%d", count),
description: count >= 0 ? "0以上だよ!" : "マイナスだよ!",
backgroundColor: count >= 0 ? Color.green : Color.red
)
}
}
// MARK: - ViewAction
extension ContentCore.Action {
// LocalのViewActionからActionに変換する関数。
// Stateを変更したりするものではない。
// LocalでないActionは外部から変更できてしまうのは変わらないため、あまり使い所はないかも。
static func view(_ localAction: ContentCore.View.Action) -> Self {
switch localAction {
case .decrement:
return decrement
case .increment:
return increment
}
}
}
// MARK: -
extension ContentCore {
static let live = Store(
initialState: ContentCore.State(),
reducer: ContentCore.reducer,
environment: ContentCore.Environment()
)
}
// MARK: - preview
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentCore.View(
store: ContentCore.live
)
}
}
解説
scopeによるViewStateとActionの取り出し
2つ目のWithViewStoreでは、scopeによりStoreから{ $0.view }
を呼び出すクロージャによってview
プロパティの値を取り出しています。そしてactionではContentCore.Action.view
を指定し、ViewActionからActionに変換する関数を渡しています。
WithViewStore(
store.scope(
state: { $0.view },
action: ContentCore.Action.view
)
)
その他
TCAのサンプルでViewState
と検索してSwiftUIのサンプルだけ見ると詳しくわかるはずです
Discussion