初识 The Composable Architecture(一)
目录
Reducer
public enum CounterAction {
case decrTapped
case incrTapped
}
func counterReducer(
state: AppState, action: CounterAction
) -> AppState {
var copy = state
switch action {
case .decrTapped:
copy.count -= 1
case .incrTapped:
copy.count += 1
}
return copy
}
上面的代码是个标准的 reducer 结构:当收到 incrTapped 时,修改全局状态(计数加一),再返回新的全局状态。为什么称这样的结构为 reducer?
参考这样的 函数签名:
func reduce<Result>(
_ initialResult: Result,
_ nextPartialResult: (Result, Self.Element) throws -> Result
) rethrows -> Result
其中 reducer 对应的正是 reduce 中的 nextPartialResult 部分,这也是我们将它称为 reducer 的原因。
Sequence 协议中还有 另一种 reduce 签名:
func reduce<Result>(
into initialResult: Result,
_ updateAccumulatingResult: (inout Result, Self.Element) throws -> ()
) rethrows -> Result
不难得出:(A, B) -> A 可以改写为 (inout A, B) -> Void。在工程实践中,AppState 迭代一段时间后,可能是个很大的结构体,使用 inout 可提升性能。
func counterReducer(value: inout AppState, action: CounterAction) {
switch action {
case .decrTapped:
value.count -= 1
case .incrTapped:
value.count += 1
}
}
纯函数(pure function)通常满足两点:
- 相同输入必然产生相同输出
- 没有副作用(不修改外部对象)
counterReducer 就是一个典型的纯函数。纯函数(我觉得)最吸引人的特点就是写单元测试很方便:不依赖外部环境,单元测试只要给定输入和断言输出即可,测试简单且稳定。
再看这样两个 reducer:primeModalReducer 和 favoritePrimesReducer。
public enum PrimeModalAction {
case saveFavoritePrimeTapped
case removeFavoritePrimeTapped
}
public func primeModalReducer(state: inout AppState, action: PrimeModalAction) {
switch action {
case .removeFavoritePrimeTapped:
state.favoritePrimes.removeAll(where: { $0 == state.count })
case .saveFavoritePrimeTapped:
state.favoritePrimes.append(state.count)
}
}
public enum FavoritePrimesAction {
case deleteFavoritePrimes(IndexSet)
}
public func favoritePrimesReducer(state: inout AppState, action: FavoritePrimesAction) {
switch action {
case let .deleteFavoritePrimes(indexSet):
state.favoritePrimes.remove(atOffsets: indexSet)
}
}
那么如果一个 app 的逻辑由这三个 reducer 组成,我们就需要把这三种 reducer 对应的 action 也组合在一块,不难想到:
enum AppAction {
case counter(CounterAction)
case primeModal(PrimeModalAction)
case favoritePrimes(FavoritePrimesAction)
}
三个 reducer 的函数签名也得一并调整:
func counterReducer(state: inout AppState, action: AppAction) {
switch action {
case .counter(.decrTapped):
state.count -= 1
case .counter(.incrTapped):
state.count += 1
default:
break
}
}
func primeModalReducer(state: inout AppState, action: AppAction) {
switch action {
case .primeModal(.removeFavoritePrimeTapped):
state.favoritePrimes.removeAll(where: { $0 == state.count })
state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.count)))
case .primeModal(.saveFavoritePrimeTapped):
state.favoritePrimes.append(state.count)
state.activityFeed.append(.init(timestamp: Date(), type: .addedFavoritePrime(state.count)))
default:
break
}
}
func favoritePrimesReducer(state: inout AppState, action: AppAction) {
switch action {
case let .favoritePrimes(.deleteFavoritePrimes(indexSet)):
let removedPrimes = indexSet.map { state.favoritePrimes[$0] }
state.favoritePrimes.remove(atOffsets: indexSet)
for prime in removedPrimes {
state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(prime)))
}
default:
break
}
}
得出整个 app 最大的 reducer:
func appReducer(value: inout AppState, action: AppAction) -> Void {
switch action {
case .counter(.decrTapped):
value.count -= 1
case .counter(.incrTapped):
value.count += 1
case .primeModal(.saveFavoritePrimeTapped):
value.favoritePrimes.append(value.count)
value.activityFeed.append(.init(timestamp: Date(), type: .addedFavoritePrime(value.count)))
case .primeModal(.removeFavoritePrimeTapped):
value.favoritePrimes.removeAll(where: { $0 == value.count })
value.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(value.count)))
case let .favoritePrimes(.deleteFavoritePrimes(indexSet)):
let removedPrimes = indexSet.map { value.favoritePrimes[$0] }
value.favoritePrimes.remove(atOffsets: indexSet)
for prime in removedPrimes {
value.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(prime)))
}
}
}
整个 app reducer 只是把三种局部 reducer 里的逻辑拷贝过来。可以想象,随着业务的迭代,整个 app reducer 会迅速膨胀。如果三个 reducer 能像乐高积木一样,组成一个最大的 app reducer 就好了!
How to be composable?
当 app reducer 收到某个 action 时,它如果可以直接把 action 分发到每个 reducer,让每个 reducer 独立决定是否处理这个 action 就好了!顺着这个思路,我们可以写出这样的 combine 函数:
func combine<Value, Action>(
_ reducers: (inout Value, Action) -> Void...
) -> (inout Value, Action) -> Void {
return { value, action in
for reducer in reducers {
reducer(&value, action)
}
}
}
那么,app reducer 可简化为:
let appReducer = combine(
counterReducer,
primeModalReducer,
favoritePrimesReducer
)
回看现在的 counterReducer,总感觉哪里怪怪的:它知道得太多了!它只需要知道它负责把一个 inout Int 加加减减就够了!它只需要在意 .incrTapped 和 .decrTapped 这两个 action 就行了!最终目标是 func counterReducer(state: inout Int, action: CounterAction) { ... },但我们先只简化 State,Action 暂时保留 AppAction。
// 最终目标:func counterReducer(state: inout Int, action: CounterAction) { ... }
func counterReducer(state: inout AppState, action: AppAction) {
switch action {
case .counter(.decrTapped):
state.count -= 1
case .counter(.incrTapped):
state.count += 1
default:
break
}
}
简化 State
这一步我们想要这样的 counterReducer:(inout Int, AppAction) -> Void,整个 app reducer 是 (inout AppState, AppAction) -> Void。我们需要新定义个 transform 函数来帮助完成这样的类型转换:
func transform<LocalValue, GlobalValue, Action>(
_ reducer: @escaping (inout LocalValue, Action) -> Void
) -> (inout GlobalValue, Action) -> Void {
return { globalValue, action in
// Call the reducer
}
}
这里先用 transform 占位,实际我们会用 pullback 来完成这个映射。所有的局部 reducer 都得通过这种变换,变为 (inout AppState, AppAction) -> Void,才能不破坏上面多个小 reducer combine 为一个大 reducer 的逻辑。
我们需要 f: (GlobalValue) -> LocalValue 这样的变化,然后再调用 reducer:
func pullback<LocalValue, GlobalValue, Action>(
_ reducer: @escaping (inout LocalValue, Action) -> Void,
_ f: @escaping (GlobalValue) -> LocalValue
) -> (inout GlobalValue, Action) -> Void {
return { globalValue, action in
var localValue = f(globalValue)
reducer(&localValue, action)
}
}
这样的变化,可取名叫 pullback,意味从局部 reducer 拉回至全局的 reducer。
上面的实现仅改变了 localValue,并没有把 localValue 的改变反射回 globalValue 上,还需新增 s: (inout GlobalValue, LocalValue) -> Void。f 更名为 get,s 更名为 set。
func pullback<LocalValue, GlobalValue, Action>(
_ reducer: @escaping (inout LocalValue, Action) -> Void,
get: @escaping (GlobalValue) -> LocalValue,
set: @escaping (inout GlobalValue, LocalValue) -> Void
) -> (inout GlobalValue, Action) -> Void {
return { globalValue, action in
var localValue = get(globalValue)
reducer(&localValue, action)
set(&globalValue, localValue)
}
}
在 Swift 中,一对 getter 和 setter 可以用 WritableKeyPath 来简化:
func pullback<LocalValue, GlobalValue, Action>(
_ reducer: @escaping (inout LocalValue, Action) -> Void,
value: WritableKeyPath<GlobalValue, LocalValue>
) -> (inout GlobalValue, Action) -> Void {
return { globalValue, action in
reducer(&globalValue[keyPath: value], action)
}
}
struct FavoritePrimesState {
var favoritePrimes: [Int]
var activityFeed: [AppState.Activity]
}
func favoritePrimesReducer(state: inout FavoritePrimesState, action: AppAction) {
switch action {
case let .favoritePrimes(.deleteFavoritePrimes(indexSet)):
let removedPrimes = indexSet.map { state.favoritePrimes[$0] }
state.favoritePrimes.remove(atOffsets: indexSet)
for prime in removedPrimes {
state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(prime)))
}
default:
break
}
}
extension AppState {
var favoritePrimesState: FavoritePrimesState {
get {
FavoritePrimesState(
favoritePrimes: self.favoritePrimes,
activityFeed: self.activityFeed
)
}
set {
self.favoritePrimes = newValue.favoritePrimes
self.activityFeed = newValue.activityFeed
}
}
}
func primeModalReducer(state: inout AppState, action: AppAction) {
switch action {
case .primeModal(.removeFavoritePrimeTapped):
state.favoritePrimes.removeAll(where: { $0 == state.count })
state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.count)))
case .primeModal(.saveFavoritePrimeTapped):
state.favoritePrimes.append(state.count)
state.activityFeed.append(.init(timestamp: Date(), type: .addedFavoritePrime(state.count)))
default:
break
}
}
// 直接操作必须要用到的状态,剔除多余的状态
func counterReducer(state: inout Int, action: AppAction) {
switch action {
case .counter(.decrTapped):
state -= 1
case .counter(.incrTapped):
state += 1
default:
break
}
}
let appReducer = combine(
pullback(counterReducer, value: \.count),
primeModalReducer,
pullback(favoritePrimesReducer, value: \.favoritePrimesState)
)
简化 Action
counterReducer 中仅使用 AppAction 中 counter 下的 actions,其他 enum case 默认不处理。这样也是易错的,app reducer 收到所有的 action 通通都会发送给这三个局部 reducer,如果局部 reducer 处理了不属于自己职责范围内的 action,就会出现 BUG!
func counterReducer(state: inout Int, action: CounterAction) {
switch action {
case .decrTapped:
state -= 1
case .incrTapped:
state += 1
}
}
同样地,我们也要把 counterReducer 能感知到的 action 简化至它仅需的 CounterAction!
我们需要这样的变换:
func pullback<Value, GlobalAction, LocalAction>(
_ reducer: @escaping (inout Value, LocalAction) -> Void,
action: WritableKeyPath<GlobalAction, LocalAction?>
) -> (inout Value, GlobalAction) -> Void {
return { value, globalAction in
guard let localAction = globalAction[keyPath: action]
else { return }
reducer(&value, localAction)
}
}
为什么 action: WritableKeyPath<GlobalAction, LocalAction?> 中 LocalAction 是可选呢?
Swift 中没有类似的 enum KeyPath 概念:
struct EnumKeyPath<Root, Value> {
let embed: (Value) -> Root
let extract: (Root) -> Value?
}
通过手动创建计算属性后,我们可以看到 getter 始终返回的是可选值。
enum AppAction {
case counter(CounterAction)
...
var counter: CounterAction? {
get {
guard case let .counter(value) = self else { return nil }
return value
}
set {
guard case .counter = self, let newValue = newValue else { return }
self = .counter(newValue)
}
}
...
}
合并两种 pullback
func pullback<GlobalValue, LocalValue, GlobalAction, LocalAction>(
_ reducer: @escaping (inout LocalValue, LocalAction) -> Void,
value: WritableKeyPath<GlobalValue, LocalValue>,
action: WritableKeyPath<GlobalAction, LocalAction?>
) -> (inout GlobalValue, GlobalAction) -> Void {
return { globalValue, globalAction in
guard let localAction = globalAction[keyPath: action]
else { return }
reducer(&globalValue[keyPath: value], localAction)
}
}
(我当时第一眼看到这样的函数签名,我也是惊呆的!)
enum CounterAction {
case decrTapped
case incrTapped
}
func counterReducer(state: inout Int, action: CounterAction) {
switch action {
case .decrTapped:
state -= 1
case .incrTapped:
state += 1
}
}
enum PrimeModalAction {
case saveFavoritePrimeTapped
case removeFavoritePrimeTapped
}
func primeModalReducer(state: inout AppState, action: PrimeModalAction) -> Void {
switch action {
case .removeFavoritePrimeTapped:
state.favoritePrimes.removeAll(where: { $0 == state.count })
state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.count)))
case .saveFavoritePrimeTapped:
state.favoritePrimes.append(state.count)
state.activityFeed.append(.init(timestamp: Date(), type: .addedFavoritePrime(state.count)))
}
}
enum FavoritePrimesAction {
case deleteFavoritePrimes(IndexSet)
}
func favoritePrimesReducer(state: inout FavoritePrimesState, action: FavoritePrimesAction) -> Void {
switch action {
case let .deleteFavoritePrimes(indexSet):
let removedPrimes = indexSet.map { state.favoritePrimes[$0] }
state.favoritePrimes.remove(atOffsets: indexSet)
for prime in removedPrimes {
state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(prime)))
}
}
}
let appReducer: (inout AppState, AppAction) -> Void = combine(
pullback(counterReducer, value: \.count, action: \.counter),
pullback(primeModalReducer, value: \.self, action: \.primeModal),
pullback(favoritePrimesReducer, value: \.favoritePrimesState, action: \.favoritePrimes)
)
到这一步,counterReducer 和 favoritePrimesReducer 完完全全地专注于自己所需的 State 和 Action,这就为后续的模块化提供了便利!primeModalReducer 仍需直接操作 AppState,所以这里使用 \.self。
Higher order reducer
高阶函数(higher‑order function)的定义:接受函数/闭包作为参数,或返回函数/闭包。那么「高阶 reducer」到底长什么样?函数既把 reducer 当输入,又把 reducer 当输出,这意味着什么?事实上,我们已经遇到过了:
combine是一个高阶 reducer:它接收多个 reducer 作为输入(只要它们处理的 state 和 action 类型一致),然后把每个 reducer 都跑一遍,返回一个全新的 reducer。pullback也是一个高阶 reducer:它接收一个 reducer 作为输入,并通过对局部 state 和 action 的 KeyPath 映射,返回一个作用在更全局 state 和 action 上的新 reducer。
在下面这两个 reducer 中,你会发现他们在自己的 action 中,还额外多做了更新 activityFeed 的事情。要么在各自 action 枚举中新增更新 activityFeed 的 action,要么把这部分逻辑挪出去,交给另一个 reducer 处理。
func favoritePrimesReducer(state: inout FavoritePrimesState, action: FavoritePrimesAction) -> Void {
switch action {
case let .deleteFavoritePrimes(indexSet):
let removedPrimes = indexSet.map { state.favoritePrimes[$0] }
state.favoritePrimes.remove(atOffsets: indexSet)
for prime in removedPrimes {
state.activityFeed.append( // 更新 activity feed
.init(
timestamp: Date(),
type: .removedFavoritePrime(prime)
)
)
}
}
}
func primeModalReducer(state: inout AppState, action: PrimeModalAction) -> Void {
switch action {
case .removeFavoritePrimeTapped:
state.favoritePrimes.removeAll(where: { $0 == state.count })
state.activityFeed.append( // 更新 activity feed
.init(
timestamp: Date(),
type: .removedFavoritePrime(state.count)
)
)
case .saveFavoritePrimeTapped:
state.favoritePrimes.append(state.count)
state.activityFeed.append( // 更新 activity feed
.init(
timestamp: Date(),
type: .addedFavoritePrime(state.count)
)
)
}
}
下面演示把这段更新逻辑挪到高阶 reducer 中,所以请理解为局部 reducer 不再写 activityFeed(否则会重复记录)。我们可以自定义一个高阶 reducer,名叫 activityFeed,放入更新 activityFeed 的逻辑。
func activityFeed(
_ reducer: @escaping (inout AppState, AppAction) -> Void
) -> (inout AppState, AppAction) -> Void {
return { state, action in
switch action {
case .counter:
break
case .primeModal(.removeFavoritePrimeTapped):
state.activityFeed.append(
.init(
timestamp: Date(),
type: .removedFavoritePrime(state.count)
)
)
case .primeModal(.saveFavoritePrimeTapped):
state.activityFeed.append(
.init(
timestamp: Date(),
type: .addedFavoritePrime(state.count)
)
)
case let .favoritePrimes(.deleteFavoritePrimes(indexSet)):
let removedPrimes = indexSet.map { state.favoritePrimes[$0] }
for prime in removedPrimes {
state.activityFeed.append(
.init(
timestamp: Date(),
type: .removedFavoritePrime(prime)
)
)
}
}
reducer(&state, action)
}
}
let appReducer: (inout AppState, AppAction) -> Void = combine(
pullback(counterReducer, value: \.count, action: \.counter),
pullback(primeModalReducer, value: \.self, action: \.primeModal),
pullback(favoritePrimesReducer, value: \.favoritePrimesState, action: \.favoritePrimes)
)
let withActivityFeed = activityFeed(appReducer)
不难看出:高阶 reducer 的作用像一种 state 和 action 过滤器!在这样的架构下,打日志自然是方便许多的!
func logging<Value, Action>(
_ reducer: @escaping (inout Value, Action) -> Void
) -> (inout Value, Action) -> Void {
return { value, action in
reducer(&value, action)
print("Action: \(action)")
print("State:")
dump(value)
print("---")
}
}
let withLogging = logging(activityFeed(appReducer))
Store
Reducer 定义了状态变化的核心逻辑,它本质上只是一个函数,那谁负责持有状态并分发出去呢?是 Store!Store 是运行时核心对象,持有当前状态,接收 Action,然后调用 reducer 处理 action、更新状态,并分发至 View。
public final class Store<Value, Action>: ObservableObject {
private let reducer: (inout Value, Action) -> Void
@Published public private(set) var value: Value
public init(initialValue: Value, reducer: @escaping (inout Value, Action) -> Void) {
self.reducer = reducer
self.value = initialValue
}
public func send(_ action: Action) {
self.reducer(&self.value, action)
}
}
ViewState
struct IsPrimeModalView: View {
@ObservedObject var store: Store<AppState, AppAction>
var body: some View {
VStack {
if isPrime(store.value.count) {
Text("\(store.value.count) is prime 🎉")
if store.value.favoritePrimes.contains(store.value.count) {
Button("Remove from favorite primes") {
store.send(.primeModal(.removeFavoritePrimeTapped))
}
} else {
Button("Save to favorite primes") {
store.send(.primeModal(.saveFavoritePrimeTapped))
store.send(.counter(.decrTapped)) // ? 易错!发送不属于自己应该关注的 action
}
}
} else {
Text("\(store.value.count) is not prime :(")
}
}
}
}
...
.sheet(isPresented: self.$isPrimeModalShown) {
IsPrimeModalView(store: self.store)
}
IsPrimeModalView 中持有了全局的 store,很容易发送不属于自己应该关注的 action。如果能把 action 限定为 PrimeModalAction,这样在编译时就能发现问题所在了!再者,向 IsPrimeModalView 暴露了全局状态,View 上压根用不上这么多状态,同时也阻碍了模块的拆分。
与 reducer 全局 state、全局 action 的拆分类似,对于 Store 而言,也需要将其拆分为聚焦于局部 state 和局部 action 的局部 store!
简化 State
我们可以这样做 (Value) -> LocalValue 的变换:
func map<LocalValue>(_ f: @escaping (Value) -> LocalValue) -> Store<LocalValue, Action> {
return Store<LocalValue, Action>(
initialValue: f(self.value),
reducer: { localValue, action in
self.send(action)
let updatedLocalValue = f(self.value)
localValue = updatedLocalValue
}
)
}
那我们新建一个 playground 来看下运行效果:
let store = Store<Int, Void>(initialValue: 0, reducer: { count, _ in
count += 1
})
store.send(()) // value: 1
store.send(()) // value: 2
store.send(()) // value: 3
store.send(()) // value: 4
store.send(()) // value: 5
store.value // value: 5
let newStore = store.map { $0 }
newStore.value // new value: 5
newStore.send(()) // new value: 6
newStore.send(()) // new value: 7
newStore.send(()) // new value: 8
newStore.value // new value: 8
store.value // value: 8
通过运行结果可以看出:newStore 收到的 action 不光会更新自己的 value,还会同步更新至 store。因为 map 创建的 newStore 不是独立的「副本」,而是转发到原始 store 的「映射」:
newStore.send(())会执行它的 reducer。- 这个 reducer 里第一句是
self.send(action),也就是把 action 发送给原始 store。 - 原始 store 的 reducer 会把 value 加 1。
- 然后
newStore再用f(self.value)把原始 store 的最新值同步到自己的 localValue。
所以 newStore 的每次 send 都会更新原始 store.value,因此最后原始 value 也变成 8。
这其实正是我们想要的效果:局部 store 收到 action 后产生的状态变化能正确投射到全局 store 上!
那反过来呢?全局 store 的状态没法与局部 store 的状态同步!
store.value // value: 8
store.send(()) // value: 9
store.send(()) // value: 10
store.send(()) // value: 11
newStore.value // new value: 8
store.value // value: 11
现在的局部 store 只能把自己的动作转发给全局 store(因为它直接调用了全局 store 的 send),所以能把变化传上去;但它没有办法从全局 store 接收后续的状态更新。如果局部 store 能监听全局 store 的状态变化,就可以把全局的新状态同步到局部状态,从而保持一致。
在 Swift 中,@Published 是 Combine 的属性包装器,它会生成一个投影值 projectedValue,用 $ 访问。这个投影值的类型是 Published<Value>.Publisher,它遵循 Publisher 协议,所以 $value 本质上就是一个 Publisher。局部 store 用 self.$value.sink 订阅全局 value 的发布流。每当全局 value 变化,就通过 f 映射成 LocalValue,并更新局部 localStore.value,最后把订阅存到 localStore.cancellable,保证监听持续存在。
func map<LocalValue>(
_ f: @escaping (Value) -> LocalValue
) -> Store<LocalValue, Action> {
let localStore = Store<LocalValue, Action>(
initialValue: f(self.value),
reducer: { localValue, action in
self.send(action)
localValue = f(self.value)
}
)
localStore.cancellable = self.$value.sink { [weak localStore] newValue in
localStore?.value = f(newValue)
}
return localStore
}
简化 Action
类似地,如果我们知道 f: @escaping (LocalAction) -> Action 这样的变换,就可以把 Store<Value, Action> 变为 Store<Value, LocalAction>。
public func map<LocalAction>(
f: @escaping (LocalAction) -> Action
) -> Store<Value, LocalAction> {
return Store<Value, LocalAction>(
initialValue: self.value,
reducer: { value, localAction in
self.send(f(localAction))
value = self.value
}
)
}
合并两种 map
并改名为 view,因为该方法返回的不是一个「独立的新 store」,而是原 store 的一个投影!通过 toLocalValue 把全局 Value 映射成局部 LocalValue,局部 store 只看到「局部视角」的状态。局部 store 只能发送 LocalAction,再被 toGlobalAction 转成全局 Action。也就是说,局部 store 只能发一个局部动作集合。数据源仍是全局 value。局部 store 订阅 $value,始终跟随全局 state 更新;它不拥有自己的状态源。
public func view<LocalValue, LocalAction>(
value toLocalValue: @escaping (Value) -> LocalValue,
action toGlobalAction: @escaping (LocalAction) -> Action
) -> Store<LocalValue, LocalAction> {
let localStore = Store<LocalValue, LocalAction>(
initialValue: toLocalValue(value),
reducer: { localValue, localAction in
self.send(toGlobalAction(localAction))
localValue = toLocalValue(self.value)
}
)
localStore.cancellable = $value.sink { [weak localStore] newValue in
localStore?.value = toLocalValue(newValue)
}
return localStore
}
回顾一下现在 Store 的代码:
public final class Store<Value, Action>: ObservableObject {
private let reducer: (inout Value, Action) -> Void
@Published public private(set) var value: Value
private var cancellable: Cancellable?
public init(initialValue: Value, reducer: @escaping (inout Value, Action) -> Void) {
self.reducer = reducer
self.value = initialValue
}
public func send(_ action: Action) {
self.reducer(&self.value, action)
}
public func view<LocalValue, LocalAction>(
value toLocalValue: @escaping (Value) -> LocalValue,
action toGlobalAction: @escaping (LocalAction) -> Action
) -> Store<LocalValue, LocalAction> {
let localStore = Store<LocalValue, LocalAction>(
initialValue: toLocalValue(value),
reducer: { localValue, localAction in
self.send(toGlobalAction(localAction))
localValue = toLocalValue(self.value)
}
)
localStore.cancellable = $value.sink { [weak localStore] newValue in
localStore?.value = toLocalValue(newValue)
}
return localStore
}
}
以本节开头提到的 IsPrimeModalView 为例,让我们看看如何修改:
public typealias PrimeModalState = (count: Int, favoritePrimes: [Int])
public struct IsPrimeModalView: View {
@ObservedObject var store: Store<PrimeModalState, PrimeModalAction>
public init(store: Store<PrimeModalState, PrimeModalAction>) {
self.store = store
}
public var body: some View {
VStack {
if isPrime(self.store.value.count) {
Text("\(self.store.value.count) is prime 🎉")
if self.store.value.favoritePrimes.contains(self.store.value.count) {
Button("Remove from favorite primes") {
self.store.send(.removeFavoritePrimeTapped)
}
} else {
Button("Save to favorite primes") {
self.store.send(.saveFavoritePrimeTapped)
}
}
} else {
Text("\(self.store.value.count) is not prime :(")
}
}
}
}
...
.sheet(isPresented: self.$isPrimeModalShown) {
IsPrimeModalView(
store: self.store
.view( // 投射出一个局部 Store
value: { ($0.count, $0.favoritePrimes) },
action: { .primeModal($0) }
)
)
}
结语
叽里咕噜说了一大堆,第一部分就写到这里吧,目前为止展示的示例代码可在这里看。
下次介绍 reducer 在收到 action 后还可以执行一些副作用(Side Effects)。比如说,我收到一个 action,需要发送一个网络请求,等网络请求结束后,根据结果再执行不同的 action。