1. Transforming a store’s action

我们现在已经改进了在使用可观察的Store对象的视图中访问state的模块化和人机工程学, 但我们不能将任何视图提取到独立的模块中,因为每个视图的store仍然依赖于AppAction。因此,听起来我们需要另一个操作来将发送全局actions的store转换为发送本地actions的store。我们发现我们可以转换store的value,那么我们也可以转换store的actions吗?

首先,这意味着什么? 目前,我们在外部处理actions and the store的唯一时间是通过调用send方法向store发送action的时候。如果我们能发送局部actions到那个方法而不是全局actions,然后在store的某个地方,也许它可以自动地将局部actions包装成全局actions

让我们从为这个转换准备另一个函数签名开始。我们将从另一个未命名的方法开始,以便专注于使转换工作。

final class Store<Value, Action>: ObservableObject {
  …
  func ___()
}

再说一次,我们想退回一家全新的商店。

final class Store<Value, Action>: ObservableObject {
  …
  func ___() -> Store
}

我们希望这个store能够处理比当前store更本地化的操作。

final class Store<Value, Action>: ObservableObject {
  …
  func ___<LocalAction>() -> Store<Value, LocalAction>
}

我们可以再次打开函数体并立即返回Store<Value, LocalAction>

final class Store<Value, Action>: ObservableObject {
  …
  func ___<LocalAction>() -> Store<Value, LocalAction> {
    return Store<Value, LocalAction>(
      initialValue: <#Value#>,
      reducer: <#(inout Value, LocalAction) -> Void#>
    )
  }
}

为了给store提供一个初始值,我们只需传递当前的self.value即可。类型不变。

initialValue: self.value,

现在,我们只需要弄清楚如何实现另一个奇怪的、类似于reducer的函数。让我们打开闭包。

initialValue: self.value,
  reducer: { value, localAction in

}

在这里,当我们转换一个store的值时,我们所做的主要工作是调用根storeself.send方法并使用其更新的self.value以相应地更新sub-store的值。为了调用self.send在这个方法中,我们需要一个Action,但我们只有一个LocalAction。 如果我们能把LocalActions转换成Actions,我们应该能够实现这个。听起来像是另一个函数!

让我们引入另一个fLocalAction到Action

func ___<LocalAction>(
  _ f: (LocalAction) -> Action
) -> Store<Value, LocalAction> {

在将localAction传递给store之前,我们可以使用这个flocalAction转换为更全局的action

reducer: { value, localAction in
    self.send(f(localAction))
}

但是我们需要标记f为逃逸,因为它被store捕获了。

func ___<LocalAction>(
  f: @escaping (LocalAction) -> Action
) -> Store<Value, LocalAction> {

最后,我们需要用self.value重新分配输入输出的可变值。

reducer: { value, localAction in
    self.send(f(localAction))
    value = self.value
}

好吧,可以正常编译,但这又是一个奇怪的reducer。它取决于store的内部行为来改变它的值,然后它会重新赋值给可变的值即使它根本没用那个值。

但撇开奇怪不谈,这个函数有一个我们熟悉的形状吗?

让我们来看看它的签名:

// ((LocalAction) -> Action) -> ((Store<_, Action>) -> Store<_, LocalAction>)

将通用名称归纳为A和B:

// ((B) -> A) -> ((Store<A, _>) -> Store<B, _>)

并从Store中归纳出上下文来得到一个非常清晰的画面。

// ((B) -> A) -> (F<A>) -> F<B>)

这是我们在Point-Free上遇到过多次的函数签名,我们将操作命名为pullback

// pullback: ((A) -> B) -> (F<B>) -> F<A>)

这是我们在深入研究“逆变性”时首次探索的函数形状,我们探索了一种逆转函数箭头的组合形式,就像我们在这里看到的那样。

所以我们可能会调用这个操作pullback:

public func pullback<LocalAction>(
  f: @escaping (LocalAction) -> Action
) -> Store<Value, LocalAction> {

然而,由于类似于我们在Store上描述的map操作的原因,我们不喜欢调用这个pullback方法。它具有所有相同的问题,因为它作用于引用类型,并依赖于对象的行为,而不仅仅是纯粹的数学属性。

相反,我们可以将其命名为“view”,因为它返回一个只能发送本地操作集的store视图。

final class Store<Value, Action>: ObservableObject {
  …
  public func view<LocalAction>(
    f: @escaping (LocalAction) -> Action
  ) -> Store<Value, LocalAction> {
    return Store<Value, LocalAction>(
      initialValue: self.value,
      reducer: { value, localAction in
        self.send(f(action))
        value = self.value
    }
    )
  }
}

2. Combining view functions

这个函数可以将全局操作中的store转换为更多本地操作中的store,但现在还不是这样。它仍然需要处理对更多全局store的本地store的订阅,也许我们应该将这两个view函数合并到一个一次性完成所有工作的函数中。

如果将这些函数紧密地粘贴在一起,就可以将所有action转换嵌入到同样处理state转换的方法中。首先,我们可以引入LocalAction泛型,并将这两个转换函数都作为输入。

public func view<LocalValue, LocalAction>(
  _ f: @escaping (Value) -> LocalValue,
  _ g: @escaping (LocalAction) -> Action

现在很清楚了,同时,变换函数的方向是完全相反的。

让我们更新参数名以使内容更具可读性。

func view<LocalValue, LocalAction>(
  value toLocalValue: @escaping (Value) -> LocalValue,
  action toGlobalAction: @escaping (LocalAction) -> Action

现在我们需要更新所有对视图转换函数的调用,并处理从局部actions到全局actions的转换。

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(self.value)
    reducer: { localValue, localAction in
      self.send(toGlobalAction(localAction))
      localValue = toLocalValue(self.value)
  }
  )
  localStore.cancellable = self.$value.sink { [weak localStore] newValue in
    localStore?.value = toLocalValue(newValue)
  }
  return localStore
}

3. Focusing on favorite primes actions

我们的应用无法正常编译,因为无论“ContentView.swift”使用Store的视图函数,它只传递一个转换函数的值。让我们通过为每个动作传递一个transform函数来再次进行编译。因为我们所有的视图都期望使用AppActionstore,所以我们可以使用标识函数,拼写为{$0}。

在初始化计数器屏幕的地方,可以做以下更改:

NavigationLink(
  "Counter demo",
  destination: CounterView(
    store: self.store.view(
      value: { ($0.count, $0.favoritePrimes) },
      action: { $0 }
    )
  )
)

在初始化收藏素数屏幕的地方,我们可以做以下更改。

NavigationLink(
  "Favorite primes",
  destination: FavoritePrimesView(
    store: self.store.view(
      value: { $0.favoritePrimes },
      action: { $0 }
    )
  )
)

最后,在主模态视图被构造的地方,我们也可以更新它对视图的调用:

IsPrimeModalView(
  Store: self.store
    .view(
      value: { ($0.count, $0.favoritePrimes) },
      action: { $0 }
  )
)

好了,又可以正常编译了,但是我们的视图仍然与全局AppActions一起工作。让我们更新视图以使用更多的本地actions

我们可以从FavoritePrimesView开始:

struct FavoritePrimesView: View {
  @ObservedObject var store: Store<[Int], AppAction>

并将其更新为与FavoritePrimesAction一起工作:

struct FavoritePrimesView: View {
  @ObservedObject var store: Store<[Int], FavoritePrimesAction>

它打破了我们的变异:

.onDelete { indexSet in
  self.store.send(.favoritePrimes(.deleteFavoritePrimes(indexSet)))
  self.store.send(.counter(.incrTapped))
}

🛑 Type ‘FavoritePrimesAction’ has no member ‘favoritePrimes’ 🛑 Type ‘FavoritePrimesAction’ has no member ‘counter’

对于第一个问题,解决方法很简单:我们可以解除action的嵌套:

self.store.send(.deleteFavoritePrimes(indexSet))

第二个错误是很好的错误! favorite质数视图发送了一个不应该发送的counter操作,因此,我们可以删除这行代码,并欣赏这个视图变得多么集中。

我们还有一件事要解决。初始化FavoritePrimesView:

NavigationLink(
  "Favorite primes",
  destination: FavoritePrimesView(
    store: self.store.view(
      value: { $0.favoritePrimes },
      action: { $0 }
    )
  )
)

🛑 ‘Store<[Int], AppAction>’ is not convertible to ‘Store<[Int], FavoritePrimesAction>’

我们需要将应用程序操作的store,生成一个应用程序操作的store视图。我们可以通过将FavoritePrimesAction传递给action闭包,并将其嵌入到AppActionfavoritePrimes case中来实现:

NavigationLink(
  "Favorite primes",
  destination: FavoritePrimesView(
    store: self.store.view(
      value: { $0.favoritePrimes },
      action: { AppAction.favoritePrimes($0) }
    )
  )
)

我们甚至可以使用类型推断来简化事情:

NavigationLink(
  "Favorite primes",
  destination: FavoritePrimesView(
    store: self.store.view(
      value: { $0.favoritePrimes },
      action: { .favoritePrimes($0) }
    )
  )
)

现在甚至有了一个很好的对称。

4. Extracting our first modular view

在我们更新所有其他视图之前,让我们停下来欣赏一下我们所做的:FavoritePrimesView现在已经尽可能地集中了。
它不知道AppState:它只能访问它渲染的最喜欢的素数的[Int]。而且它没有办法发送AppActions:它只能发送FavoritePrimesActions。这就是我们一直在努力追求的模块化。这意味着我们最终可以将这个视图提取到它自己的模块中。

虽然它可以位于自己的模块中,但现在让我们将它移到“FavoritePrimes”模块。我们可以剪切粘贴:

struct FavoritePrimesView: View {
  @ObservedObject var store: Store<[Int], FavoritePrimesAction>

  var body: some View {
    …
  }
}

🛑 Use of undeclared type ‘View’

但是它依赖于一堆其他模块,包括Combine、SwiftUI,甚至我们的“ComposableArchitecture”框架,所以让我们导入它们。

import ComposableArchitecture
import SwiftUI

所有的东西都是编译的,所以它们是独立的,但是我们需要让所有的东西都是public,也就是:结构体本身和它的body

public struct FavoritePrimesView: View {
  @ObservedObject var store: Store<[Int], FavoritePrimesAction>

  public var body: some View {
    …
  }
}

如果我们尝试构建我们的应用程序,我们会得到一个错误。

🛑 ‘FavoritePrimesView’ initializer is inaccessible due to ‘internal’ protection level

在将视图移动到模块时,我们遇到了一个类似于将prime modal state移动到模块时的问题:Swift自动为我们生成的成员初始化式是internal,但我们需要一个public初始化式来在我们的应用程序中生成值。

所以我们需要从头开始定义它:

public struct FavoritePrimesView: View {
  @ObservedObject var store: Store<[Int], FavoritePrimesAction>

  public init(store: Store<[Int], FavoritePrimesAction>)
    self.store = store
  }

  public var body: some View {
    …
  }
}

有了它,一切都构建起来了,我们真的,用编译器检查的方式,把这个视图从应用程序的其他部分分离出来了。这样做伴随着一些显式初始化式形式的样板文件,但就像我们在本系列早期采用的样板文件一样,它完全是机械编写的,未来的Xcode版本将能够为我们编写它。 这是完全值得的,以确保我们的view有一个非常有限的能力,以获得更多的全局actions和state

5. Focusing on prime modal actions

我们还有一些视图可以从AppAction中分离出来,比如IsPrimeModalView:

struct IsPrimeModalView: View {
  @ObservedObject var store: Store<PrimeModalState, AppAction>

它只需要发送PrimeModalActions

struct IsPrimeModalView: View {
  @ObservedObject var store: Store<PrimeModalState, PrimeModalAction>

它通过消除更多的嵌套来简化操作的调用:

Button("Remove from favorite primes") {
   self.store.send(.removeFavoritePrimeTapped)
}
…
Button("Save to favorite primes") {
  self.store.send(.saveFavoritePrimeTapped)
}

在视图初始化的地方,我们必须提供一个限制其操作的store

IsPrimeModalView(
  store: self.store.view(
    value: { ($0.count, $0.favoritePrimes) }
    action: { .primeModal($0) }
  )
)

更多的本地操作需要嵌入到AppAction中。

现在我们有另一个完全孤立的view

让我们将视图及其isPrime助手函数剪切并粘贴到“PrimeModal”模块中。

struct IsPrimeModalView: View {
  @ObservedObject var store: Store<PrimeModalState, PrimeModalAction>

  var body: some View {
    …
  }
}

func isPrime(_ p: Int) -> Bool {
  …
}

同样,我们要添加一些导入。

import ComposableArchitecture
import SwiftUI

我们必须公开视图的界面。

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

  public var body: some View {
    …
  }
}

我们需要定义一个公共初始化式。

public init(store: Store<PrimeModalState, PrimeModalAction>) {
  self.store = store
}

过程和上次完全一样。当我们构建我们的应用程序时,所有的编译都很好,整个过程只花了几分钟。

6. Focusing on counter actions

现在,我们已经从应用程序中提取了两个视图到它们自己的模块中,并极大地限制了它们的表面积。它们不再能够访问所有的应用state和应用actions

我们还有一个视图,但它比其他视图更复杂一些,所以让我们尝试处理它,看看会发生什么。

如果我们看我们的counter视图,它是一个大的。

struct CounterView: View {
  @ObservedObject var store: Store<CounterViewState, AppAction>

它仍然适用于所有的app动作。

它需要访问它所显示的计数器的CounterActions,并且它需要访问PrimeModalActions以便将它们传递给IsPrimeModalView。它不需要访问FavoritePrimesActions,也不应该有将它们发送到store的能力。但我们如何限制视图发送这些动作,同时保留发送counter动作或主要模态动作的能力?

我们可以用一种类似于限制主模态所能访问的state的方式来实现:通过创建一个中间类型。但这一次,我们想要定义的不是一个结构体,而是一个enum,它处理的每个操作都有一个case。

enum CounterViewAction {
  case counter(CounterAction)
  case primeModal(PrimeModalAction)
}

有了这个定义,我们可以换出store的动作类型:

struct IsPrimeModalView: View {
  @ObservedObject var store: Store<CounterViewState, CounterViewAction>

没有什么需要改变的。因为case名称匹配,所以我们能够进行这个重构,并保持视图主体完全完整。

事情还没有完全建立,尽管,因为CounterView被初始化与错误的store类型。

NavigationLink(
  "Counter demo",
  destination: CounterView(
    store: self.store.view(
      value: { ($0.count, $0.favoritePrimes) },
      action: { $0 }
    )
  )
)

🛑 Cannot convert value of type ‘CounterViewAction’ to closure result type AppAction

action块现在传递了一个CounterViewAction,我们需要返回一个AppAction。没有AppAction case会嵌入CounterViewAction,但是,我们可以切换容器动作,打开每个case,并在适当的AppAction中重新包装它们。

NavigationLink(
  "Counter demo",
  destination: CounterView(
    store: self.store.view(
      value: { ($0.count, $0.favoritePrimes) },
      action: {
        switch $0 {
        case let .counter(action):
          return AppAction.counter(action)
        case let .primeModal(action):
          return AppAction.primeModal(action)
        }
      }
    )
  )
)

这已经足够满足编译器的要求了,但是内联编写有很多内容,特别是当我们致力于简化视图时。幸运的是,这是非常简单的逻辑,我们将进一步简化它。

好的,counter视图已经完全准备好被移动到“counter”模块。我们可以剪切和粘贴我们需要的部分,包括视图、PrimeAlert包装结构、中间的CounterViewAction和几个助手。

struct PrimeAlert: Identifiable {
  …
}

enum CounterViewAction {
  …
}

struct CounterView: View {
  @ObservedObject var store: Store<CounterViewState, CounterViewAction>
  …

  var body: some View {
    …
  }
}

func nthPrime(_ n: Int, callback: @escaping (Int?) -> Void) -> Void {
  …
}

func ordinal(_ n: Int) -> String {
  …
}

让我们添加这些导入。

import ComposableArchitecture
import SwiftUI

我们有一个编译器错误,因为我们依赖于PrimeModalAction,它存在于自己的模块中。

enum CounterViewAction {
  case counter(CounterAction)
  case primeModal(PrimeModalAction)

🛑 Use of undeclared type ‘PrimeModalAction’

因此,在导入框架之前,我们也需要更新框架以依赖于这个模块。

import PrimeModal

But we still have one error:

🛑 Use of unresolved identifier ‘wolframAlpha’

因为我们还需要将“WolframAlpha.swift”移动到模块中,因为nthPrime助手使用它。

好了,框架似乎可以编译了,让我们来审计公共接口。中间的计数器容器操作需要是public,这样我们的应用程序就可以创建一个专注于这些操作的store

public typealias CounterViewState = (count: Int, favoritePrimes: [Int])

public enum CounterViewAction {
  …
}

最后,视图还是需要是公共的,并给出一个显式的公共初始化式。

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

  var body: some View {
    …
  }
}

内部的助手都不需要公开。它们甚至可以变成私有的!

一切都可以正常构建,但是counter视图已经被完全提取到它自己的模块中。我们甚至可以运行应用程序来确保一切正常,就像以前一样,即使每个屏幕都存在于它自己的模块中。

7. Next time: what’s the point?

好了,现在我们已经完成了模块状态管理系列文章开始时要做的事情。 首先,我们定义了“模块化”的含义:我们确定模块化是将代码移动到字面上的Swift模块的能力,这样它就完全与应用程序的其余部分隔离。然后,我们展示了我们的reducers已经是多么模块化,通过将每个reducer分解成一个自己的框架。最后,为了模块化视图,我们在Store类型上发现了两种组合形式:一种允许我们将现有store集中在state的某个子集上,另一种允许我们将现有store集中在actions的某个子集上。这些操作允许我们将所有的视图从更全局的、应用级的关注点中分离出来,并将它们移动到各自独立的模块中。

但每当我们在point - free上结束这些探索时,我们总是会停下来反思,然后问:“有什么意义?”

为了满足我们对模块化的要求,我们必须:

  • 在Xcode中为每个组件创建一个新框架。这可能是一件麻烦的事情,并可能导致复杂的项目构建设置。
  • 识别并移动应该移动到模块的代码,并审计模块需要公开的公共接口
  • 围绕公共初始化器添加样板,有时甚至为state引入中间struct,为actions引入中间enum

我们真的需要这种级别的模块化吗? 为什么我们不能把东西移到单独的文件中,然后引入私有和文件私有访问呢? 如果可以,为什么不避免额外的样板文件呢? 这是相当多的额外工作。这真的值得去做吗?

我们已经在整个系列中说过了,但是我们还要再说一遍:我们绝对相信这是值得做的。

每个模块现在都高度关注一小部分应用状态、逻辑和视图,由编译器强制执行。您的队友和未来的自己现在可以阅读这些模块,并以一种可能需要更多时间和精力的方式一目了然地理解它们。

的确,您可以将代码组织到更独立的文件中,并且引入private and fileprivate访问可以限制泄漏的代码数量。但是,它并没有阻止这些文件访问模块中的所有其他内容。在一个大型应用目标中,这可能是很多。

成本是一些创建模块、移动代码和编写有限数量的样板文件的前期工作。但好处在于都在以后所节省下来。

我们还可以通过展示这些屏幕如何独立运行来测试这种模块化。例如,我们可以在playground上加载任何这些屏幕,并在那里进行试验,就像它们是自己的应用程序一样使用它们……下一次!