Reducer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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?
参考这样的 函数签名 :
1 2 3 4 func reduce <Result >( _ initialResult : Result , _ nextPartialResult : (Result , Self .Element ) throws -> Result ) rethrows -> Result
其中 reducer 对应的正是 reduce 中的 nextPartialResult 部分,这也是我们将它称为 reducer 的原因。
Sequence 协议中还有 另一种 reduce 签名 :
1 2 3 4 func reduce <Result >( into initialResult : Result , _ updateAccumulatingResult : (inout Result , Self .Element ) throws -> () ) rethrows -> Result
不难得出:(A, B) -> A 可以改写为 (inout A, B) -> Void。在工程实践中,AppState 迭代一段时间后,可能是个很大的结构体,使用 inout 可提升性能。
1 2 3 4 5 6 7 8 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 也组合在一块,不难想到:
1 2 3 4 5 enum AppAction { case counter(CounterAction ) case primeModal(PrimeModalAction ) case favoritePrimes(FavoritePrimesAction ) }
三个 reducer 的函数签名也得一并调整:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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 函数:
1 2 3 4 5 6 7 8 9 10 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 可简化为:
1 2 3 4 5 let appReducer = combine( counterReducer, primeModalReducer, favoritePrimesReducer )
回看现在的 counterReducer,总感觉哪里怪怪的:它知道得太多了!它只需要知道它负责把一个 inout Int 加加减减就够了!它只需要在意 .incrTapped 和 .decrTapped 这两个 action 就行了!最终目标是 func counterReducer(state: inout Int, action: CounterAction) { ... },但我们先只简化 State,Action 暂时保留 AppAction。
1 2 3 4 5 6 7 8 9 10 11 12 13 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 函数来帮助完成这样的类型转换:
1 2 3 4 5 6 7 func transform <LocalValue , GlobalValue , Action >( _ reducer : @escaping (inout LocalValue , Action ) -> Void ) -> (inout GlobalValue , Action ) -> Void { return { globalValue, action in } }
这里先用 transform 占位,实际我们会用 pullback 来完成这个映射。所有的局部 reducer 都得通过这种变换,变为 (inout AppState, AppAction) -> Void,才能不破坏上面多个小 reducer combine 为一个大 reducer 的逻辑。
我们需要 f: (GlobalValue) -> LocalValue 这样的变化,然后再调用 reducer:
1 2 3 4 5 6 7 8 9 10 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。
1 2 3 4 5 6 7 8 9 10 11 12 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 来简化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 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!
1 2 3 4 5 6 7 8 9 func counterReducer (state : inout Int , action : CounterAction ) { switch action { case .decrTapped: state -= 1 case .incrTapped: state += 1 } }
同样地,我们也要把 counterReducer 能感知到的 action 简化至它仅需的 CounterAction!
我们需要这样的变换:
1 2 3 4 5 6 7 8 9 10 11 12 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 概念:
1 2 3 4 struct EnumKeyPath <Root , Value > { let embed: (Value ) -> Root let extract: (Root ) -> Value ? }
通过手动创建计算属性后,我们可以看到 getter 始终返回的是可选值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 1 2 3 4 5 6 7 8 9 10 11 12 13 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) } }
(我当时第一眼看到这样的函数签名,我也是惊呆的!)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 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 处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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) ) ) } } } 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) ) ) } }
下面演示把这段更新逻辑挪到高阶 reducer 中,所以请理解为局部 reducer 不再写 activityFeed(否则会重复记录)。我们可以自定义一个高阶 reducer,名叫 activityFeed,放入更新 activityFeed 的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 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 过滤器!在这样的架构下,打日志自然是方便许多的!
1 2 3 4 5 6 7 8 9 10 11 12 13 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。
1 2 3 4 5 6 7 8 9 10 11 12 13 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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)) } } } 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 的变换:
1 2 3 4 5 6 7 8 9 10 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 来看下运行效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 let store = Store <Int , Void >(initialValue: 0 , reducer: { count, _ in count += 1 }) store.send(()) store.send(()) store.send(()) store.send(()) store.send(()) store.value let newStore = store.map { $0 }newStore.value newStore.send(()) newStore.send(()) newStore.send(()) newStore.value store.value
通过运行结果可以看出: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 的状态同步!
1 2 3 4 5 6 7 8 store.value store.send(()) store.send(()) store.send(()) newStore.value store.value
现在的局部 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,保证监听持续存在。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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>。
1 2 3 4 5 6 7 8 9 10 11 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 更新;它不拥有自己的状态源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 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 为例,让我们看看如何修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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( value: { ($0 .count, $0 .favoritePrimes) }, action: { .primeModal($0 ) } ) ) }
结语 叽里咕噜说了一大堆,第一部分就写到这里吧,下次再介绍 reducer 在收到 action 后还可以执行一些副作用(Side Effects)。比如说,我收到一个 action,需要发送一个网络请求,等网络请求结束后,根据结果再执行不同的 action。