可组合 Effect 标识符

Swift Documentation

这个 ComposableEffectIdentifier 是一个小型辅助库,用于 Composable Architecture (TCA)。它允许在定义 Effect 标识符时改善用户体验。

它为此目的提供了两个工具:一个 @EffectID 属性包装器,以及一个 namespace() 高阶 reducer,它允许多个类似的 store 实例在同一进程中运行,而无需微观管理正在进行的 Effect 标识符。

@EffectID 属性包装器。

当在 TCA 中使用长期存在的 effect 时,我们需要提供一些可哈希的值来跨同一 reducer 的多次运行来标识它们。如果我们启动一个 timer effect,我们需要为该 effect 提供一个标识符,以便在不再需要它时检索并取消该 effect。

任何 Hashable 值都可以用作 effect 标识符。TCA 的作者建议利用 Swift 类型系统,通过定义临时的、无属性的 Hashable 结构体。这种结构体的任何值都等于自身,并且碰撞风险有限,因为这些类型是局部定义的。

例如,在某个 Reducer 的代码块内,可以定义

struct TimerID: Hashable {}

然后我们可以使用此类型的任何值作为 effect 标识符

switch action {
  case .start:
    return Effect.timer(id: TimerId(), every: 1, on: environment.mainQueue)
      .map { _ in .tick }
      
  case .stop:
    return .cancel(id: TimerID())
   
  case .tick:
    state.count += 1
    return .none
}

这工作得很好。但是,当我们仅仅需要一个 Hashable 值时,调用 TimerID() 并创建一个完整的类型感觉有点笨拙。

@EffectID 属性包装器允许以绝对清晰的意图定义标识符

@EffectID var timerID

访问此值会返回一个唯一且稳定的 Hashable 值,可用于标识 effect

switch action {
  case .start:
    return Effect.timer(id: timerID, every: 1, on: environment.mainQueue)
      .map { _ in .tick }
      
  case .stop:
    return .cancel(id: timerID)
   
  case .tick:
    state.count += 1
    return .none
}

为了在某些 reducer 中局部定义,需要 Swift >=5.4(更准确地说,需要 Swift >=5.5,因为 Swift 5.4 中存在无值局部属性包装器的错误)。

通过为属性分配一些 Hashable 值,您可以利用附加数据来增强生成的标识符

@EffectID var timerID = state.identifier

请注意,如果 @EffectID 在不同的位置定义,即使共享相同的用户定义值,它们也不会相等

@EffectID var id1 = "A"
@EffectID var id2 = "A"
// => id1 != id2

当使用命名空间时,甚至可以避免大多数时候使用用户定义的值。

命名空间

在其当前的实现中,核心 TCA 库在某些配置中使用起来可能不太方便,尤其是在开发基于文档的应用程序时。在这种应用程序中,每个文档都由一个根 Store 表示。每个文档应该不知道在同一时间打开的其他文档的存在。在这种应用程序中,同一类型的根 Store 的多个实例可能在同一进程中同时运行。当在 reducers 中使用像 Hashable 结构体这样的局部标识符来标识 effect 时,可能会创建冲突,其中一个 store 实例可能会取消来自另一个 store 的 effect(因为正在进行的 effect 在内部存储在一个通用的、顶级的字典中)。

一种解决方案是在 StateEnvironment 中传播一些文档特定的标识符,但这将需要将此标识符附加到每个 effect 标识符,以便正常工作。此外,在每个不相关的功能中“泄漏”这样的标识符会阻碍功能隔离和可重用性(TCA 的核心原则)。

同样的精神,组合具有正在进行的 effect 的功能集合也很麻烦,因为我们需要注入一些元素特定的标识符来辨别来自不同行的相似 effect。通常,行功能相对简单,因此行标识符的渗透不太明显,但它仍然存在,而行功能最终应该在某些与列表无关的上下文中工作。

幸运的是,ComposableEffectIdentifier 附带一个功能,可以极大地帮助解决此类问题。它与 @EffectID 属性包装器结合使用,并需要使用它来声明 effect 标识符。任何 Reducer 都可以用某个 Hashable 值进行命名空间化。此值用于使用上下文数据来增强 @EffectID 标识符(您可以将其视为用户提供的值,但来自“顶部”)。命名空间沿着 Reducer 的树向下传播,并与更深层的命名空间组合。

Reducer 命名空间

您可以使用 .namespace<ID>(_ id: ID) 高阶 reducer 来命名空间化 reducer,这不会更改源 Reducer 的泛型签名。id 可以直接提供,也可以作为来自 StateEnvironment 的函数或 KeyPath 提供。在程序的所有执行过程中,id 值对于分支应该是恒定的。对于基于文档的应用程序,您很可能使用一些稳定值来命名空间化根 reducer,该值唯一标识文档。

自动命名空间

提供了 forEach pullback 的两个半重载。它们都命名为 forEachNamespace,但否则它们与它们的 forEach 对应项共享相同的参数。这些 reducer 的工作方式类似于 forEach,但它们也使用元素的标识符(或字典键)来命名空间化它们的局部 reducer,从而隔离每个 pullback reducer 的 effect。因此,这些局部 reducer 可以使用 @EffectID 属性包装器独立地定义它们的 effect 标识符,而无需携带外部标识符。

Identified 状态

TCA 已经附带了一个 Identified 包装器,可以将任何值包装成一个 Identifiable 值。@EffectID 的使用导致功能变得越来越独立于外部标识符。因此,将无标识符功能的 StateIdentified 包装器包装起来可能很方便,例如将其包含到 IdentifiedArrayOf<Identified<State, ID>> 中。由于使功能可标识化可能是该过程的唯一结果,因此还提供了 forEachNamespace 的重载,以在一个调用中直接 pullback、命名空间化和标识无标识符的 reducer。当 GlobalStateIdentifiedArrayOf<Identified<LocalState, ID>>,并且无标识符的 reducer 在 LocalState 上工作时,此重载可用。

示例应用

为了演示命名空间化的 reducers 和 @EffectID 属性包装器的强大功能,该库附带了一个示例应用程序,该应用程序展示了一个巧妙的技巧:实现了一个 LonelyTimer TCA 功能。此功能处理一个向下计数到零的计时器,具有一些启动/停止功能。LonelyTimer 功能不知道外部世界。它处理它的计数,仅此而已。它本身没有标识符。它有一个名称,但只是出于礼貌。

围绕这个 LonelyTimer 功能,构建了一个名为 ManyTimers 的应用程序。此应用程序处理任意数量的计时器,但无需触及 LonelyTimer 的代码。ManyTimers 应用程序还提供 4 种风格:一个 shoebox 应用程序,其中十几个计时器同时托管在一个列表中,适用于 iOS 和 macOS,以及一个基于文档的应用程序,其中每个文件处理一个计时器,再次适用于 iOS 和 macOS。基于文档的应用程序可以同时打开多个文件,这在某种程度上类似于将它们并排托管在列表中。

使用这两个应用程序,可以同时运行和交互多个计时器,每个计时器都是隔离的。只定义了一个 effect 标识符,在 LonelyTimer 级别。

文档

ComposableEffectIdentifier 的 API 的最新文档可在此处 获取

安装

添加

.package(url: "https://github.com/tgrapperon/composable-effect-identifier", from: "0.0.1")

到您的 Package.swift 中的 Package 依赖项,然后

.product(name: "ComposableEffectIdentifier", package: "composable-effect-identifier")

到您的目标的依赖项。

鸣谢和感谢

作者 (@tgrapperon) 特别感谢 @iampatbrown,他提供了最初的反馈,从而塑造了这个库,当然还要感谢 @mbrandonw@stephencelis,感谢他们为 TCA 和他们其他令人惊叹的开源项目所做的出色工作。

许可证

此库在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE