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)通常满足两点:

  1. 相同输入必然产生相同输出
  2. 没有副作用(不修改外部对象)

counterReducer 就是一个典型的纯函数。纯函数(我觉得)最吸引人的特点就是写单元测试很方便:不依赖外部环境,单元测试只要给定输入和断言输出即可,测试简单且稳定。

再看这样两个 reducer:primeModalReducerfavoritePrimesReducer

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) -> Voidf 更名为 gets 更名为 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 中仅使用 AppActioncounter 下的 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)
)

到这一步,counterReducerfavoritePrimesReducer 完完全全地专注于自己所需的 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。