初识 The Composable Architecture(一)

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

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

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

再看这样两个 reducer:primeModalReducerfavoritePrimesReducer

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 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 函数来帮助完成这样的类型转换:

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
// Call the reducer
}
}

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

到这一步,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 处理。

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( // 更新 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 的逻辑。

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)) // ? 易错!发送不属于自己应该关注的 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 的变换:

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(()) // 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 的状态同步!

1
2
3
4
5
6
7
8
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,保证监听持续存在。

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( // 投射出一个局部 Store
value: { ($0.count, $0.favoritePrimes) },
action: { .primeModal($0) }
)
)
}

结语

叽里咕噜说了一大堆,第一部分就写到这里吧,下次再介绍 reducer 在收到 action 后还可以执行一些副作用(Side Effects)。比如说,我收到一个 action,需要发送一个网络请求,等网络请求结束后,根据结果再执行不同的 action。