1. View models and view stores

让我们从Store中删除与ObservableObject的一致性,这样我们就可以停止通知视图的状态变化。我们还将删除该值的公共访问权限,因为视图不应该使用它来呈现其UI:

public final class Store<Value, Action> /* : ObservableObject */ {
  …
  @Publisher private var value: Value

我们将属性保持为@Published,因为我们确实希望将对值所做的任何更改重放到所有派生子store中,而拥有一个值的发布者使得这一点非常容易实现。

现在,让我们看看如何实现这个次要对象,它实际上负责通知状态变化的视图。我们应该给它起什么名字呢?我们将从iOS社区长期使用的术语中获得一些灵感,甚至苹果公司最近也开始在WWDC会议的SwiftUI上使用这个术语。术语“view model”长期以来被用作精确描述视图的领域的一种方法。它在业务逻辑和视图之间提供了一个很好的抽象。业务逻辑可能有一些不直接映射到UI的概念,比如正在运行的API请求的加载状态,而视图有一些非常具体的东西,用户可以看到,比如按钮的启用或禁用状态。

因此,使用“view model”作为灵感,我们将把我们引入架构的新概念称为“view store”:

ViewStore {
}

我们应该把它看作是一个可以直接使用视图的store。在stores保存我们应用程序所有混乱的业务逻辑的地方,因此可能包含比我们的视图所关心的更多的信息,而ViewStore将只保存这个视图所关心的领域。如果它不需要这些信息,它甚至不需要持有其子视图的域。

我们希望这个对象与SwiftUI交互,这样它就可以触发视图的重渲染,因此它需要是一个类:

class ViewStore {
}

它还需要是一个ObservableObject,以便SwiftUI能够接收到到它的通知:

public final class ViewStore: ObservableObject {
}

要成为一个可观察对象,它需要持有一个发布者,该发布者在store中的值即将发生变化时发出。 SwiftUI提供了一种简单的方法,可以同时在类中声明存储的值,并在值发生变化时自动ping那个发行者:

final class ViewStore<Value>: ObservableObject {
  @Published public fileprivate(set) var value: Value
}

由于类在Swift中的工作方式,我们也必须提供一个初始化器:

public final class ViewStore<Value>: ObservableObject {
  @Published public private(set) var value: Value

  init(initialValue: Value) {
    self.value = initialValue
  }
}

这是ViewStore的基础。它只是简单地将一些值包装在一个类中,并将其公开为一个可观察对象。

使它成为我们架构中有用的概念的真正工作是实现从常规Stores创建ViewStores的方法。因此,给定一个理解值和操作的特定领域的store,我们希望能够从它派生一个ViewStore,以防止通知域外的状态变化。

首先,让我们假设我们将通过一个计算属性从一个store派生一个view store:

extension Store {
  var view: ViewStore<Value> {
    ???
  }
}

在试图弄清楚如何实现这个属性之前,我们应该提到我们在Store上已经有了一个视图方法,它是我们用来从父视图传递到子视图的焦点Store的方法。我们在考虑ViewStore的概念之前就给这个方法命名了,所以回过头来看,同时拥有这两个名称似乎有点令人困惑。

让我们重命名原始的view方法,以便我们可以为ViewStore转换释放该名称。但是,我们把它重命名为什么呢?
我们可以把它叫做focus,但这和视图很相似,所以看起来有点奇怪。我们也可以笼统地称之为“transform”,但这并不能说明我们是在把全局Store转变为本地Store。 因此,我们喜欢将其命名为scope,因为它不会与view相混淆,它有助于我们理解应用于我们的Store的某种限制过程:

public func scope<LocalValue, LocalAction>(
  value toLocalValue: @escaping (Value) -> LocalValue,
  action toGlobalAction: @escaping (LocalAction) -> Action
) -> Store<LocalValue, LocalAction> {
  …
}

现在我们可以将这种聚焦操作想象成不是“查看”商店,而是将商店“限定范围”到子域。

我们来求一下view方法的签名:

extension Store {
  var view: ViewStore<Value> {
    ???
  }
}

构造ViewStore的方法是通过它的初始化器,它接受一个初始值:

extension Store {
  var view: ViewStore<Value> {
    ViewStore(initialValue: self.value)
  }
}

这样就可以编译,但这当然是不对的。因为现在ViewStore将永远不会更新它的值,因此它永远不会通知一个SwiftUI视图它需要重新渲染。

我们需要做一些类似于我们在scope方法中所做的事情,也就是订阅store的值中的所有更改,并将它们重放到ViewStore中。

extension Store {
  var view: ViewStore<Value> {
    let viewStore = ViewStore(initialValue: self.value)

    self.$value.sink(receiveValue: { value in
      viewStore.value = value
    })

    return viewStore
  }
}

现在正在编译,但是我们收到了关于sink未使用返回值的警告。 此方法返回一个可取消的值,该值允许我们在需要时停止从发布者接收值。特别是,如果我们在ViewStore中存储一个可取消对象:

public final class ViewStore<Value>: ObservableObject {
  …
  fileprivate var cancellable: Cancellable?

并在调用sink时赋值:

viewStore.cancellable = self.$value.sink(receiveValue: { value in
  viewStore.value = value
})

然后我们保证,当ViewStore被释放时,我们将停止从这个发布者接收值。


2. View store performance

我们离这个方法的有用实现越来越近了,但是有些地方仍然不太对。我们仍然没有做任何工作来确保ViewStore不会向其视图过量释放值。就目前的情况而言,每次全局store改变时,它所有关联的view store也会改变,从而触发视图重新计算。

幸运的是,我们可以使用Combine提供的一些机制来确保view store不会产生任何重复值。我们只需要使用removeduplicate方法:

viewStore.cancellable = self.$value
  .removeDuplicates()
  .sink(receiveValue: { value in
    viewStore.value = value
})

但这行不通,因为我们的值不一定相等。让我们约束这个扩展,使它这样:

extension Store where Value: Equatable {
  var view: ViewStore<Value> {
    …
  }
}

现在我们有了这个属性的实际实现。这允许我们从任何store中派生一个ViewStore,只要我们的状态是相等的。

通常,当一个API要求某种类型是可衡平的,当我们不能使用一个可衡平的类型时,会提供一种与之密切相关但可替代的API形式。例如,removeDuplicates方法有一个重载,你可以显式地提供当副本被移除时的条件:

.removeDuplicates(by: <#T##(Equatable, Equatable) -> Bool#>)

我们也希望这样做,因为我们有时会使用元组来表示特性的状态,但不幸的是,元组是不能相等的。因此,我们将创建一个重载视图,允许我们这样做:

extension Store {
  public func view(
    removeDuplicates predicate: @escaping (Value, Value) -> Bool
  ) -> ViewStore<Value> {
    let viewStore = ViewStore(initialValue: self.value)
    viewStore.cancellable = self.$value
      .removeDuplicates(by: predicate)
      .sink { newValue in viewStore.value = newValue }
    return viewStore
  }
}

甚至其他的实现也可以写成这个形式:

extension Store where Value: Equatable {
  public var view: ViewStore<Value> {
    self.view(removeDuplicates: ==)
  }
}

在继续之前,让我们先解决一个内存泄漏问题。现在可取消对象在这个闭包中持有viewStore,但viewStore本身也持有可取消对象作为属性。这和我们在确定存储范围时遇到的问题是一样的,修复方法是在闭包中弱保存viewStore:

.sink { [weak viewStore] newValue in viewStore?.value = newValue }

这是我们对stores view的实现。

使用ViewStores

当我们试图从store中访问值时,这会导致编译器错误。我们希望通过用一个适当构造的ViewStore来替换所有这些store的使用,我们将避免不必要的重新计算和视图的渲染。

让我们从最简单的特性开始,然后往回研究。我们想提一下我们在关于可组合架构的章节中多次提到的一些事情,即由于我们在模块化应用程序方面的努力,我们有机会增量地修复这些编译器错误。我们可以只专注于一个模块并独立地构建它,而不是试图一次性修复整个应用程序。这使得我们更容易对我们的架构和库进行这些类型的改变。这只能归功于我们的模块化能力,而可组合体系结构使模块化变得非常容易。

FavoritePrimes包中,我们有以下错误:

🛑 Property type ‘Store<[Int], FavoritePrimesAction>’ does not match that of the ‘wrappedValue’ property of its wrapper type ‘ObservedObject’

这是因为stores不再是可观察的。相反,我们需要一个view store。我们可以在视图中声明一个新字段:

public struct FavoritePrimesView: View {
  let store: Store<FavoritePrimesState, FavoritePrimesAction>
  @ObservedObject var viewStore: ViewStore<FavoritePrimesState>

现在,store and view store使用相同的状态,但这只是这个视图的巧合。它不需要这样,通常也不应该这样。

在视图的主体中,我们现在需要通过view store来访问当前值,而不是从store中访问当前值:

ForEach(self.viewStore.value.favoritePrimes, id: \.self) { prime in
  Text("\(prime)")
}
…
.alert(item: .constant(self.viewStore.value.alertNthPrime)) {

这个文件中的最后一个错误与我们还没有创建view store有关:

🛑 Return from initializer without initializing all stored properties

要创建view store,我们只需要查看store来派生它的view store:

public init(store: Store<FavoritePrimesState, FavoritePrimesAction>) {
  print("FavoritePrimesView.init")
  self.store = store
  self.viewStore = store.view
}

这不能工作,因为FavoritePrimesState是一个元组,而在Swift中元组是不相等的。也许有一天这个问题会得到解决,但在那之前会有一个特别的解决方案。 Swift标准库为一些大小的元组定义了一个==操作符的重载,我相信最多有6个组件。 因此,我们可以使用重载view函数,它接受一个显式的等式函数:

self.viewStore = self.store.view(removeDuplicates: ==)

这将使内容进行编译,现在我们可以放心,这个视图只有在其局部状态发生变化时才会重新计算自己,特别是收藏的质数和alert。对全局应用程序状态的任何其他更改都不会对该视图产生影响。

接下来,让我们看看PrimeModal。它有一个类似的错误,我们已经看到:

🛑 Property type ‘Store<PrimeModalState, PrimeModalAction>’ (aka ‘Store<(count: Int, favoritePrimes: Array), PrimeModalAction>’) does not match that of the ‘wrappedValue’ property of its wrapper type ‘ObservedObject’

这是因为Store不再是可观察的,所以我们需要使用一个ViewStore:

public struct IsPrimeModalView: View {
  let store: Store<PrimeModalState, PrimeModalAction>
  @ObservedObject var viewStore: ViewStore<PrimeModalState>

接下来,我们需要在初始化器中分配viewStore属性:

public init(store: Store<PrimeModalState, PrimeModalAction>) {
  print("IsPrimeModalView.init")
  self.store = store
  self.viewStore = self.store.view(removeDuplicates: ==)
}

然后,我们必须使用viewStore来代替从store中访问状态:

public var body: some View {
  print("IsPrimeModalView.body")
  return VStack {
    if isPrime(self.viewStore.count) {
      Text("\(self.viewStore.value.count) is prime 🎉")
      if self.viewStore.favoritePrimes.contains(self.viewStore.count) {
        Button("Remove from favorite primes") {
          self.store.send(.removeFavoritePrimeTapped)
        }
      } else {
        Button("Save to favorite primes") {
          self.store.send(.saveFavoritePrimeTapped)
        }
      }
    } else {
      Text("\(self.viewStore.value.count) is not prime :(")
    }
  }
}

现在我们的PrimeModal软件包正在构建中。

切换到Counter,我们会看到所有相同的编译器错误。我们需要去掉 observable store ,而使用viewStore

public struct CounterView: View {
  let store: Store<CounterViewState, CounterViewAction>
  @ObservedObject var viewStore: ViewStore<CounterViewState>

我们需要为这个视图构建视图存储:

public init(store: Store<CounterViewState, CounterViewAction>) {
  print("CounterView.init")
  self.store = store
  self.viewStore = self.store.view
}

这一次我们不需要使用removeDuplicates,因为CounterViewState遵循Equatable。

最后有6个地方我们需要通过viewStore而不是store访问视图的状态。


3. Counter view performance

但我们可以改进这一点。现在我们的视图不需要访问整个CounterViewState,它只使用其中的一个子集。为了精简这个视图的状态到它最基本的要素,也就是视图需要完成它的工作的最低限度,让我们在视图存储中放入一个空类型,然后慢慢地添加回它需要的属性:

public struct CounterView: View {
  struct State: Equatable {
  }
  …
  @ObservedObject var viewStore: ViewStore<State>

每当我们试图访问store中的某些状态时,这会立即给我们一个编译器错误。例如,我们可以看到我们需要访问isNthPrimeButtonDisabled布尔值,所以让我们把它添加到store的值类型中:

struct State: Equatable {
  let isNthPrimeButtonDisabled: Bool
}

接下来我们看到它需要isPrimeModalShown布尔值:

struct State: Equatable {
  let isNthPrimeButtonDisabled: Bool
  let isPrimeModalShown: Bool
}

然后我们看到我们需要访问alertNthPrime:

struct State: Equatable {
  let alertNthPrime: PrimeAlert?
  let isNthPrimeButtonDisabled: Bool
  let isPrimeModalShown: Bool
}

然后我们看到我们需要count:

struct State: Equatable {
  let alertNthPrime: PrimeAlert?
  let count: Int
  let isNthPrimeButtonDisabled: Bool
  let isPrimeModalShown: Bool
}

这就修复了视图主体中的编译器错误。因此,我们只需要访问四个字段:alertNthPrime、count、isNthPrimeButtonDisabled和isPrimeModalShown

值得注意的是,我们不需要访问favoritePrimes数组,该数组位于CounterViewState类型中。这意味着对应用程序状态的该字段所做的任何更改都不会触发视图重新计算自身。

为了获得完整的视图编译,我们需要构造一个 view store来支持这个视图,我们首先将store限定为它所关心的状态的基本要素,然后将它转换为一个view store:

self.viewStore = self.store
  .scope(
    value: { counterViewState in
      State(
        alertNthPrime: counterViewState.alertNthPrime,
        count: counterViewState.count,
        isNthPrimeButtonDisabled: counterViewState.isNthPrimeButtonDisabled,
        isPrimeModalShown: counterViewState.isPrimeModalShown
      )
  },
    action: { $0 }
)
  .view

你可能不喜欢在初始化器中包含所有这些逻辑,所以我们可以做的一件简单的事情是在文件底部创建一个方便的初始化器:

extension CounterView.State {
  init(counterViewState: CounterViewState) {
    self.alertNthPrime = counterViewState.alertNthPrime
    self.count = counterViewState.count
    self.isNthPrimeButtonDisabled = counterViewState.isNthPrimeButtonDisabled
    self.isPrimeModalShown = counterViewState.isPrimeModalShown
  }
}

哈,但这暴露了命名的一点尴尬。我们现在有一个CounterViewState和一个CounterView.State。状态:名称相同但状态不同。嵌套在CounterView内部的新State结构描述了CounterView需要观察的所有状态,而CounterViewState包含运行整个计数器特性所需的所有状态,其中包括主模态的状态。

也许CounterViewState一开始就不是正确的名字。我们定义它来描述CounterState和PrimeModalState的联合,这是运行计数器特性所需的所有状态。所以也许CounterFeatureState更合适:

public struct CounterFeatureState: Equatable {

我们可以更新CounterView.State初始化参数:

extension CounterView.State {
  init(counterFeatureState: CounterViewState) {
    self.alertNthPrime = counterFeatureState.alertNthPrime
    self.count = counterFeatureState.count
    self.isNthPrimeButtonDisabled = counterFeatureState.isNthPrimeButtonDisabled
    self.isPrimeModalShown = counterFeatureState.isPrimeModalShown

有了重命名,我们现在可以:

self.viewStore = self.store
  .scope(value: State.init(counterFeatureState:), action: { $0 })
  .view

当我们重命名东西时,我们也可以重命名CounterViewAction:

public enum CounterFeatureAction: Equatable {

4. View store memory management

现在计数器模块又开始构建了,但我们意外地引入了一个问题。如果我们在reducer中添加一些日志记录,我们会看到操作确实被发送到store和更新状态,但更改不会反映在UI中。

控制UI渲染的东西是view store,因为它是可观察的东西,我们刚刚改变了view store的构造所以它首先作用域store,然后views:

self.viewStore = self.store
  .scope(value: State.init(counterFeatureState:), action: { $0 })
  .view

在这个转换链中,当调用.scope时,我们创建了一个中间存储。结果是中间存储中没有任何东西,因此它会立即被释放,因此视图存储不会被通知任何变化。

为了让我们自己相信这一点,我们首先可以看到,通过查看scope实现中的这一行,scoped stores肯定会保留它们的parent stores:

let localStore = Store<LocalValue, LocalAction>(
  initialValue: toLocalValue(self.value),
  reducer: { localValue, localAction, _ in
    self.send(toGlobalAction(localAction))
    localValue = toLocalValue(self.value)
    return []
},
  environment: self.environment
)

在这里,我们通过在reducer闭包中引用self来确保派生的本地scoped store保持在父函数上。这是一件好事。这意味着如果我们在同一个store上多次调用.scope转换,我们将得到一个相互关联的连锁stores

然而,同样的情况不会发生在派生视图存储时:

public func view(
  removeDuplicates predicate: @escaping (Value, Value) -> Bool
) -> ViewStore<Value> {
  let viewStore = ViewStore(initialValue: self.value)

  viewStore.cancellable = self.$value
    .removeDuplicates(by: predicate)
    .sink { newValue in
      viewStore.value = newValue
  }

  return viewStore
}

我们需要派生view store来保存它的派生view store。我们很快就会有正确的方法来表达store和view store之间的关系,但现在,为了让事情正常工作,我们可以做一些简单的事情,通过在sink闭包中引用self:

viewStore.cancellable = self.$value
  .removeDuplicates(by: predicate)
  .sink { newValue in
    viewStore.value = newValue
    self
}

只剩下一件事需要解决。在主要的PrimeTime应用目标中,我们使用一个store作为可观察对象,这不再是正确的做法。

struct ContentView: View {
  let store: Store<AppState, AppAction>

在所有其他视图中,我们引入了一个可观察view store。但是view store应该保持什么状态呢?

struct ContentView: View {
  let store: Store<AppState, AppAction>
  @ObservedObject var viewStore: ViewStore<???>

如果我们看一下这个视图的body,它实际上不需要任何state来做它的工作。它只是一个静态视图层次结构。
意味着我们甚至根本不需要view store,我们可以删除它,让视图是静态的:

struct ContentView: View {
  let store: Store<AppState, AppAction>
//  @ObservedObject var viewStore: ViewStore<???>

如果我们检查日志,我们会看到只有当视图状态直接改变时,才会调用视图的body属性。添加最喜欢的元素不会导致counter视图重新呈现,当然也不会导致根内容视图重新呈现。事实上,在根内容视图第一次呈现之后,没有任何东西会导致它重新呈现。

我们现在已经解决了我们打算解决的问题。通过为我们的每个视图构建一个view store,我们可以确保它们只观察到整个应用程序状态变化的基本要素,从而最小化发生的视图重新计算和重新呈现的数量。


5. Adapting view stores

因为我们已经解决了性能问题,所以我们可以就此歇一歇,但我们想要深入研究。view stores的东西比你看到的要多。它不仅仅是一种提高性能或防止意外重新呈现视图的方法。它能做的远不止这些。

首先,它为我们提供了一个完美的位置来移动我们视图中的一些逻辑。例如,在PrimeModal模块中,我们有以下一行代码来决定我们想为最喜欢的素数显示保存按钮还是删除按钮:

if self.viewStore.value.favoritePrimes.contains(self.viewStore.value.count) {

这个逻辑现在还不算太坏,但如果我们能直接说:

if self.viewStore.value.isFavorite {

view stores的概念为我们提供了一个完美的地方,将逻辑从我们的视图移到一个更孤立、更容易理解的地方。我们所要做的就是改变view stores所持有的状态使之完全符合视图的需要。特别是,它想知道当前的计数是多少,以及它是否是最受欢迎的:

public struct IsPrimeModalView: View {
  struct State: Equatable {
    let count: Int
    let isFavorite: Bool
  }
  …
  @ObservedObject var viewStore: ViewStore<State>

现在视图的主体是快乐的。但是当我们构造这个view store时,我们需要首先将它作用域限制到viewstate:

self.viewStore = self.store
  .scope(
    value: State(
      count: $0.count,
      isFavorite: $0.favoritePrimes.contains($0.count)
 },
    action: { $0 }
)
  .view

主模态模块正在构建,但在视图初始化器中有很多逻辑要管理,所以让我们把它移到本地State结构体的构造器中。

extension IsPrimeModalView.State {
  init(primeModalState state: PrimeModalState) {
    self.count = state.count
    self.isFavorite = state.favoritePrimes.contains(state.count)
  }
}

然后我们可以更简单地使用它来界定我们的store,然后.view

self.viewStore = self.store
  .scope(value: State.init(primeModalState:), action: { $0 })
  .view

这很好,我们可以把逻辑移出视图,移动到一个点,在那里我们只是做一个简单的转换从一个版本的状态到另一个版本的状态。更好的是,这让我们有更多的机会跳过不必要的屏幕重新渲染。目前,该视图只会在收藏状态发生变化时重新计算自己。这意味着应用程序的其他部分可能会对我们的收藏数组进行大量更改,但这些更改都不会触发重新计算,除非它碰巧改变了我们正在查看的当前计数。

还有更多关于我们如何进行这些类型的view store转换的例子。例如,在计数器中,我们有当前状态结构体:

public struct CounterFeatureState: Equatable {
  public var alertNthPrime: PrimeAlert?
  public var count: Int
  public var favoritePrimes: [Int]
  public var isNthPrimeButtonDisabled: Bool
  public var isPrimeModalShown: Bool
}

这是我们的完美状态,但有些可能会觉得有点奇怪,我们有一个混合物的状态,它是我们应用程序的核心抽象表示(尤其是countfavoritePrimes字段),和一些内容显示在屏幕上更具描述性的state(像alertNthPrime,isNthPrimeButtonDisabled和isPrimeModalShown)。

如果需要的话,我们可以使用view store的概念来更好地分离这两种状态。让我们对isNthPrimeButtonDisabled状态做这个,这样你就能看到它是如何运行的。现在,这个字段的命名非常清楚地与它在UI中表示的内容一致,这真的很方便,因为这意味着视图代码以最简单的方式使用它:

.disabled(self.viewStore.isNthPrimeButtonDisabled)

该字段的值取决于对“第n个素数”的API请求是否正在运行。当我们试图加载该信息时,我们禁用该按钮,一旦我们得到响应,我们就重新启用它。

但是,如果在请求运行期间有更多的东西需要禁用呢? 也许我们甚至要禁用自增和自减按钮。也许我们还想在请求飞行时显示加载指示符。这些都是有效的事情,我们可以添加更多的状态来表示它们:

public struct CounterFeatureState: Equatable {
  public var alertNthPrime: PrimeAlert?
  public var count: Int
  public var favoritePrimes: [Int]
  public var isNthPrimeButtonDisabled: Bool
  public var isPrimeModalShown: Bool

  public var isIncrementButtonDisabled: Bool
  public var isDecrementButtonDisabled: Bool
  public var isLoadingIndicatorHidden: Bool
}

或者,我们可以描述状态的基本单位,所有这些状态都可以从中得到。 特别地,我们真正关心的事情是网络请求正在运行,从那里我们可以推导出所有其他的东西。所以让我们去掉这些特定于ui的字段,用ui无关字段替换它:

public struct CounterFeatureState: Equatable {
  public var alertNthPrime: PrimeAlert?
  public var count: Int
  public var favoritePrimes: [Int]
  public var isNthPrimeRequestInFlight: Bool
  public var isPrimeModalShown: Bool
  …
  public init(
    alertNthPrime: PrimeAlert? = nil,
    count: Int = 0,
    favoritePrimes: [Int] = [],
    isNthPrimeRequestInFlight: Bool = false,
    isPrimeModalShown: Bool = false
  ) {
    self.alertNthPrime = alertNthPrime
    self.count = count
    self.favoritePrimes = favoritePrimes
    self.isNthPrimeRequestInFlight = isNthPrimeRequestInFlight
    self.isPrimeModalShown = isPrimeModalShown
  }

这会破坏一些东西。我们首先看到的是CounterFeatureState上的counter属性,它负责切掉计数器特性所关心的唯一属性(与主要模态特性相反)。这个功能的reducer也应该停止担心按钮是否被禁用,而是使用更高级的概念,即网络请求是否在空中:

public typealias CounterState = (
  alertNthPrime: PrimeAlert?,
  count: Int,
  isNthPrimeRequestInFlight: Bool,
  isPrimeModalShown: Bool
)
…
var counter: CounterState {
  get { (self.alertNthPrime, self.count, self.isNthPrimeRequestInFlight, self.isPrimeModalShown) }
  set { (self.alertNthPrime, self.count, self.isNthPrimeRequestInFlight, self.isPrimeModalShown) = newValue }
}

然后reducer需要使用这个新属性:

case .nthPrimeButtonTapped:
  state.isNthPrimeRequestInFlight = true
  …

case let .nthPrimeResponse(prime):
  …
  state.isNthPrimeRequestInFlight = false
  …

最后我们要修复视图,这就是有趣的地方。view store当前准确地表达了我们的视图想要的状态,所以这里没有什么需要改变的:

struct State: Equatable {
  let alertNthPrime: PrimeAlert?
  let count: Int
  let isNthPrimeButtonDisabled: Bool
  let isPrimeModalShown: Bool
}

@ObservedObject var viewStore: ViewStore<State>

我们只需要更新转换为view store状态的初始化器:

extension CounterView.State {
  init(counterFeatureState state: CounterFeatureState) {
    self.alertNthPrime = state.alertNthPrime
    self.count = state.count
    self.isNthPrimeButtonDisabled = state.isNthPrimeRequestInFlight
    self.isPrimeModalShown = state.isPrimeModalShown
  }
}

现在这个模块正在编译,我们已经成功地使该特性的状态对UI更加不可知,同时仍然允许视图使用特定于域的状态描述。特别是,我们可以创建更多的布尔值来控制递增和递减按钮的启用/禁用状态:

struct State: Equatable {
  let alertNthPrime: PrimeAlert?
  let count: Int
  let isNthPrimeButtonDisabled: Bool
  let isPrimeModalShown: Bool
  let isIncrementButtonDisabled: Bool
  let isDecrementButtonDisabled: Bool
}
…
extension CounterView.State {
  init(counterFeatureState state: CounterFeatureState) {
    self.alertNthPrime = state.alertNthPrime
    self.count = state.count
    self.isNthPrimeButtonDisabled = state.isNthPrimeRequestInFlight
    self.isPrimeModalShown = state.isPrimeModalShown
    self.isIncrementButtonDisabled = state.isNthPrimeRequestInFlight
    self.isDecrementButtonDisabled = state.isNthPrimeRequestInFlight
  }
}
…
Button("-") { self.store.send(.counter(.decrTapped)) }
  .disabled(self.viewStore.value.isDecrementButtonDisabled)
Text("\(self.viewStore.value.count)")
Button("+") { self.store.send(.counter(.incrTapped)) }
  .disabled(self.viewStore.value.isIncrementButtonDisabled)

最酷的是,我们可以将这些新特性和功能添加到我们的视图中,而不需要对我们的reducer或应用程序状态进行任何更改。这个逻辑纯粹是视图关心的问题,因为它是应用程序状态到视图状态的纯转换。

我们现在已经看到ViewStore抽象比仅仅作为一个性能工具更有趣。这是我们塑造视图数据的一种方式,以便使我们的视图主体尽可能地简单和无逻辑。


6. conclution

目前我们的ViewStore有点不平衡,因为我们在使用ViewStore时只关注应用程序状态。这是可以理解的,因为ViewStore的整个动机是为了提高性能而最小化我们的视图所知道的状态,但我们的应用程序还有另外一面:actions!

通过扩展ViewStore的概念来考虑视图所关心的操作,我们将能够进一步凿开视图所访问的域。