ReactiveSwiftRex 0.8.12×由 Luiz Barbosa 和 Giulia Ariu 护理。安装指南×ReactiveSwiftRex 的安装指南您想将类似于以下内容添加到 Podfile 中 pod 'ReactiveSwiftRex', '~> 0.8'target 'MyApp' do pod 'ReactiveSwiftRex', '~> 0.8' end然后在您的终端内部运行 pod install,或者从 CocoaPods.app 运行。或者为了测试运行,运行以下命令pod try ReactiveSwiftRex 依赖关系SwiftRex~> 0.8.12ReactiveSwift~> 7.0.0 请查看 PodspecGitHub 仓库CocoaPods.org 上的页面ReactiveSwiftRex 0.8.12由Luiz BarbosaSwiftRex/SwiftRexGitHub 仓库 针对您最喜欢的响应式框架的单向数据流 如果您有任何关于 SwiftRex 或 Redux 以及通用编程的问题,请 SwiftRex 是一个框架,它结合了单向数据流架构和响应式编程(Combine、RxSwift 或 ReactiveSwift),为应用程序的全局状态提供中央 Store,SwiftUI 视图或 UIViewControllers 可观察、响应,还可派发来自用户交互的事件。 此模式也称为 "Redux",允许我们将应用程序视为一个单一的 纯函数,该函数接收用户事件作为输入,并根据 UI 更改返回。这种工作流程的优点将在不久的将来变得清晰。 API 文档可在以下位置找到. 在赶时间吗?已经熟悉其他 redux 实现? 没问题, 我们仍然建议阅读完整的 README, 如今,许多针对移动开发的架构和设计模式都旨在解决与《单一职责原则》(例如:庞大的 ViewController)相关的特定问题,或者提高可测试性和依赖管理。对于移动开发者而言,其他常见的挑战,如状态处理、竞态条件、模块化/组件化、线程安全性或正确处理 UI 生命周期和所有权等问题,虽然探讨较少,但同样可能对应用造成严重影响。 管理所有这些问题可能听起来是一项不可能完成的任务,需要众多模式和非常复杂的测试场景。毕竟,如何重现只有部分用户在设备上出现而开发者设备上从未发生的关键错误呢?这可能令人沮丧,我们中的大多数人可能都曾在某个时候遇到过这类问题。 这正是 SwiftRex 发光的地方,因为 强制执行单一职责原则的应用 [点击展开] 一些架构非常灵活,允许我们在任何地方添加任何代码片段。这对于仅由单人开发的小型应用来说可能没问题,但一旦项目和团队开始增长,某些层将变得非常大,承担过多的责任,隐式副作用、竞态条件和其他错误。在这种情况下,测试性也会受损,应用不同部分之间的统一性也会受损,因此查找和修复错误变得非常棘手。 SwiftRex 通过非常严格的代码放置政策来防止这种情况,政策通常由编译器强制执行。好吧,这听起来很困难且复杂,但实际上它比传统模式更容易,因为一旦您理解了这种架构,您就知道确切要做什么,您就知道基于其责任,具体要找到哪些代码行,您就知道如何测试每个组件,并且深刻理解每个层的边界。 为每一层提供清晰的测试策略(有关更多信息请参阅 TestingExtensions)[点击展开] 我们认为,架构不仅必须是可测试的,而且还应提供如何测试其每一层的明确指南。如果一个层只有一个任务,而且这个任务可以通过基于给定输入的期望输出断言来验证,那么测试将更有意义和广泛,因此当创建新功能时不会引入回归。 SwiftRex 架构中的大部分层将是纯函数,这意味着所有计算都仅从输入参数中进行,而且所有结果都将在输出中暴露,没有隐式效果或全局作用域的访问。此次测试无需模拟、存根、依赖注入或任何类型的准备,只需调用一个函数并与一个值一起使用,然后检查结果即可。 这对于 UI 层、展示层、reducers 和状态发布者都适用,因为整个链是由纯函数组成的。唯一需要依赖注入(因此需要模拟)的层是中间件,因为它是唯一依赖于服务并引发对外部世界副作用的一层。幸运的是,由于中间件是可组合的,我们可以将其拆分为非常小的独立部分,每个部分只做一项工作,这使得测试变得更加愉快和简单,因为您不必模拟数百个组件,只需注入一个即可。 我们还提供了 TestingExtensions,它允许我们使用 DSL 语法测试整个用例,该语法将验证所有 SwiftRex 层,确保没有意外副作用或动作发生,并且状态正如预期的那样逐步发生了变化。这是一种强大且有趣的方式来全面测试整个应用,只需简洁易写的几行代码。 将所有副作用封装在不可修改状态的组合/可重用中间件盒子中 [点击展开] 如果一个层需要在同一时间处理多个服务并异步响应用户请求,它们会更改状态,那么要保持这个状态的一致性和防止竞态条件就很困难。同时,也难以进行测试,因为一个效果可能会干扰另一个。 多年来,苹果和社区创建了用于访问网页、网络或设备传感器服务的一些非常棒的平台。不幸的是,其中一些框架依赖于代理模式,一些使用闭包/回调,一些使用通知中心、KVO或响应式流。这个各种通知形式的组合需要布尔标志、计数器和其他隐含状态,最终会因为竞态条件而破坏。 响应式框架有助于使这个过程更加统一和可组合,特别是在与其Cocoa扩展一起使用时。事实上,苹果也已经意识到了这一点,在2019年全球开发者大会(WWDC 2019)中,有很大一部分是专注于演示和解决这个问题,借助新引入的框架Combine和SwiftUI。 但将众多服务在响应式管道中进行组合并不总是容易,也有一些自己的陷阱,例如因一个流发出错误而导致整个管道取消,事件再入性,以及最后但同样重要的,掌握多个操作符的陡峭学习曲线。 SwiftRex大量使用了响应式编程,并且允许你根据自己的舒适度使用它。然而,我们也提供了一种更统一的方式来用1种数据类型和2个操作符组合不同的服务:中间件、`<>`操作符和`lift`操作符,所有其他操作都可以通过触发动作到自身、其他中间件或状态还原器来简化。尽管如此,你仍然可以选择创建更大的中间件并在传统的响应式流方式中处理多个源,如果你想这样做的话,但这可能对未经经验的开发者来说过于复杂,难以测试和在不同应用中重用。 由于这个主题非常广泛,最好在中间件文档中进行更详细的解释。 最小化对ViewControllers/ Presenters/ Interactors/ SwiftUI视图的依赖[点击展开] 浏览应用时传递依赖项始终是一个棘手的任务:ViewControllers的初始化器非常复杂,你必须始终考虑类是从NIB/XIB、编程或故事板创建的,然后你必须编写正确的初始化方法,不仅传入这个类所需要的所有依赖项,还要传入其子视图控制器和当你按下按钮时将要推入的下一个视图控制器所需要的依赖项。因此,你必须保持将成打的依赖项发送到你的视图,在你导航它们时。如果你不使用初始化器,而喜欢使用属性赋值,那么这些属性必须隐式解析,这并不是很好。 当然,协调器/线框模式对此有所帮助,但问题是某种方式你会将问题传送给路由器,这些路由器也需要保留他们实际上并未使用但下一个路由器将会使用的更多依赖项。你可以使用服务定位器模式,例如流行的环境方法,这实际上是一个处理问题的简单方法。但是,对这个单例进行测试可能很复杂,因为,好吧,它是一个单例。同时,有些人不喜欢隐式注入,他们更喜欢向层添加显式依赖项。 所以,解决这个问题并让每个人都满意是不可能的,对吗?其实并非如此。如果你的视图控制器只需要一个名为"Store"的单个依赖项,从中获取所需的状态并将所有用户事件分派到那里,而不实际执行任何工作,那么注入store将更容易完成,无论你是否使用显式注入或服务定位器。 好的,但仍然需要有人去做这些工作,这正是中间件执行的任务。在SwiftRex中,应该在应用入口处创建中间件,在配置并准备好依赖之后立即创建它们。然后创建所有中间件,注入它们执行工作所需的所有内容(希望每个中间件不超过2个依赖项,这样你知道它们并没有太多的责任)。最后,将它们组合起来,启动你的store。中间件可以有计时器,也可以纯粹地响应来自UI的动作,但它们是唯一具有副作用层面,因此也是唯一需要服务依赖层面的。 最后,您可以将地区、语言和界面特征添加到全局状态中,这样即使您需要在状态中创建数字和日期格式化器,也可以无需依赖注入完成,甚至在用户决定更改iOS设置时,仍能恰当地做出反应。 完全将状态、服务、变更和其他副作用从UI生命周期及其拥有树中分离出来 [点击展开] `UIViewControllers` 具有非常独特的所有权模型:你不能控制它。当它们位于导航堆栈中时、当显示标签或模态视图时,视图控制器被保留在内存中,但它们可以在任何时候释放,因此,放在视图控制器伞下的任何内容都可以被释放。我们一直使用并喜爱的 `[weak self]` 实际上有时可能是弱引用的,并且当我们“保护否则返回”时,很容易不为此事感到不安。无论视图是否显示,任何重要的任务都必须完成,因为你很容易关闭你的模态或弹出视图。虽然SwiftUI已经改善了这一点,但仍然可能从视图的闭包中启动异步任务,虽然现在这个视图是一个值类型,使这些错误变得更加困难,但仍然有可能。 SwiftRex通过强制所有和每一个副作用或异步任务都必须由中间件来完成,而不是视图来解决这个问题。而中间件的生命周期由store拥有,所以只要应用存在,我们就不会期望出现不幸的意外。 你仍然可以从你的视图中调度 "viewDidLoad"、"onAppear"、"onDisappear" 事件,以便执行任务取消,这样你得到的不是更少的而是在某种程度上增加了控制。更多信息请查看此链接 消除竞争条件 [点击展开] 当应用需要处理来自不同服务和来源的信息时,通常需要一个小的布尔标志来检查某事是否完成或失败。通常这是由于一些服务通过代理报告,一些通过闭包,还有其他一些创造性方式。使用标志同步这些多个来源,或者从并发任务中突变相同的变量或数组可能会导致非常奇怪的错误和崩溃,通常这些错误最难捕获、理解和修复。 处理锁和dispatch队列可以帮助解决这个问题,但反复以这种方式手动进行是单调的、危险的,必须编写考虑所有可能路径和时间的测试,其中一些测试最终会因为竞争条件仍然存在而变得不可靠。 通过强制所有应用程序的事件通过同一个队列,最终以一致的方式突变全局状态,SwiftRex可以防止竞态条件。首先,因为有中间件作为唯一来源的副作用和异步任务将简化对竞态条件的测试,特别是如果你让它们保持小规模,专注于单一任务。在这种情况下,你的响应将以FIFO队列的方式到来,并由所有reducer同时处理。其次,因为reducer是状态突变的大门守护者,使其无副作用对于成功和一致地突变至关重要。最后但并非最不重要的是,所有的事情都是在对动作的反应下发生的,并且可以轻松地将动作记录在日志中或放入您的事故报告中,包括谁调用了该动作,所以如果您仍发现竞态条件发生,您可以轻松地了解哪些动作正在突变状态以及这些动作从何而来。 允许更安全的类型编码风格 [点击展开] Swift泛型有点难学,还有关联类型协议。SwiftRex不需要你掌握泛型、了解协变或类型擦除,但你在这种感觉世界中越深入,你写的应用程序就越会通过编译器的验证而不是单元测试。将bug从运行时带到编译时是我们都应该接受的良好开发者的重要目标。可能更不好受一点的Swift类型系统,而不是在应用程序发布到野外后检查崩溃报告。这正好是Swift作为静态类型语言带来的心态,这种语言中,即使是nullability也是类型安全的,感谢Optional,我们现在可以安心地知道,除非我们不安全地并且明确地选择,否则我们不会访问空指针。 SwiftRex强制在所有地方使用强类型的事件/动作和状态:存储的行动派发器、中间件的动作处理器、中间件的动作输出、reducer的动作和状态输入输出以及最后存储的状态观察,整个流程是强类型的,因此编译器可以防止错误或运行时错误。 此外,中间件、reducer和存储都可以从部分状态和动作“提升”到全局状态和动作。这意味着你可以编写一个专门领域的强类型模块,例如网络可达性。你的中间件和reducer将“说话”网络领域状态和动作,例如是否连接、是Wi-Fi还是LTE、是否改变了网络连接动作等。然后你可以通过提供两个映射函数“提升”这两个组件——中间件和reducer——到你的应用程序的全局状态,一个用于提升状态,另一个用于提升动作。多亏了泛型,这个整个操作是完全类型安全的。同样可以通过“推断”主存储的存储投影来完成。存储投影实现了一个存储必须有的两个方法(输入动作和输出状态),但它不是真实的存储,而只是将全局状态和动作投影到一个更加本地化的领域,这意味着,视图事件转换成动作,视图状态转换成领域状态。 有了这些工具,我们相信,如果你愿意,你可以编写从边缘到边缘都是类型安全的的应用程序。 有助于实现模块化、组件化和项目之间的代码复用 [点击展开] 中间件应该专注于一个非常非常小的域,仅执行一种类型的工作,并以动作的形式返回。reducer应该专注于非常少的动作和状态的组合。视图应该能够访问非常小的状态部分,或者理想情况下能够访问一个视图状态,它是一个应用程序全局状态的平铺表示,使用直接映射到文本框字符串、切换的布尔值、进度条的double从0.0到1.0等原语。 然后,你可以把这三件——中间件、reducer、存储投影——提升到你的应用程序真正需要的全局状态和动作中。 SwiftRex 允许我们创建只有在需要时才能提升到全局域的微小工作单元,因此我们可以在非常具体的域中使用 Swift 框架,并用测试和 Playgrounds/SwiftUI 预览来使用而不必启动完整的 App。一旦此框架准备好,我们只需将其插入我们自己的 App,或者更好的是,多个 Apps。专注于小型域将解锁更好的抽象,并且当它从中间件(副作用)到视图时,你将拥有一个强大的工具来定义你的构建块。 强制实现唯一的真相源和正确的状态管理 [点击展开] SwiftRex 使得实现一个可信的、在屏幕之间永远不冲突或不同步的单一真相源成为可能。认为你的所有状态都在一个地方,一个包含一切的单一树,可能会有些可怕。一旦你将所有东西都聚集在一个地方,看到你需要多少状态可能会很吓人。但不用担心,这并不是你没有的东西,它已经在那里了,在 ViewController、在 Presenter、在控制服务结果的状态标志中,只是因为它太过分散,你看不到它在多大程度上。更糟糕的是,这将导致重复,因为当你需要从两个不同地方获取相同信息时,简单地复制并希望你能正确地保持它们同步要容易得多。 实际上,当你将整个 App 状态聚集到一个统一的树中时,你开始丢弃不再需要的很多东西,最终状态将比混乱的状态小。 正确编写全局状态和全局动作树可能会具有挑战性,但这确实是 App 域,推理这一点可能是工程师需要做的最重要的事情。 有关更多信息,请点击此链接 提供开发、测试和调试工具 [点击展开] 几个项目提供了 SwiftRex 工具,以帮助开发者在编写应用、测试、调试它或评估崩溃报告时。 CombineRextensions 提供 SwiftUI 扩展,用于与 CombineRex 一起工作,TestingExtensions 有“测试断言”,可以轻松愉快地解锁对用例的可测试性,InstrumentationMiddleware 允许你使用 Instruments 来查看 SwiftRex App 中发生的情况,SwiftRexMonitor 将成为Redux DevTools的Swift版本,你可以从外部 Mac 或 iOS 设备远程监控 App 的状态和动作,并注入动作来模拟副作用(例如,对 UI 测试很有用),GatedMiddleware 是一个中间件包装器,可以在运行时启用或禁用其他中间件,LoggerMiddleware 是一个非常强大的日志记录器,供开发者使用,可以轻松理解运行时发生的事情。更多中间件将被开源,例如,可以创建很好的 Crashlytics 报告,告诉你原来没有访问到的崩溃故事,并以这种方式,重建崩溃或用户报告。此外,提供代码生成工具(Sourcery 模板、Xcode 段落和模板、控制台工具),还有更高层次的 API,例如 EffectMiddlewares,它可以让我们像 Reducers 一样创建具有单个函数的中间件,以及 Handler,它允许将 Middlewares 和 Reducers 作为一个结构组在一起,以便能够同时提升它们。新依赖注入策略也将很快发布。 所有这些工具已经完成,并将在不久的将来发布,未来还有更多。 实话实说,这与编写应用程序的完全不同的方式,就像大多数反应式方法一样;但一旦你习惯了,这会让事情更有意义,并且使你能够在不同的项目之间重用更多的代码,为你提供更好的软件开发、测试、调试、日志记录工具,最后你能以前所未有的方式考虑事件、状态和突变。我保证,这将是一条不会有归途的道路,一次性的单向旅程。 反应式框架库 SwiftRex 当前支持3个主要的反应式框架 Apple Combine RxSwift ReactiveSwift 可以通过实现《ReactiveWrappers.swift》文件中可以找到的一些抽象桥梁,轻松地添加更多。为了防止向您的应用程序添加不必要的文件,SwiftRex分为4个包 SwiftRex:核心库 CombineRex:为Combine框架的实现 RxSwiftRex:为RxSwift框架的实现 ReactiveSwiftRex:为ReactiveSwift框架的实现 SwiftRex本身是不够的,所以你必须选择这三个实现中的一个。 部分 让我们通过将它们分为3个部分来了解SwiftRex的组件 概念部分 行为 状态 核心部分 存储 存储类型 真实存储 商店投射 全部组合 中间件 泛型 返回IO和执行副作用 依赖注入 中间件示例 效应中间件 缩减器 投射和提升 商店投射 提升 提升缩减器 使用闭包提升缩减器 使用KeyPath提升缩减器 提升中间件 使用闭包提升中间件 使用KeyPath提升中间件 可选转换 箭头的方向 使用KeyPaths 身份、忽略和荒谬 Xcode代码片段 概念部分 行为 状态 行为 动作表示外部(或有时内部)实体通知您的应用程序的事件。它涉及相关的输入事件。 SwiftRex 中没有“动作”协议或类型。然而,动作将会作为大多数核心数据结构的泛型参数被找到,这意味着您需要自行定义根动作类型。 从概念上讲,我们可以这样说,动作代表来自于您应用程序外部实体的某种发生的事情,这意味着用户的交互、定时器回调、Web服务的响应、位置服务的回调以及其他框架,一些内部实体也可以启动动作。例如,当 UIKit 结束加载您的视图时,我们可以将 viewDidLoad 看作一个动作,如果我们对这个事件感兴趣。对于 SwiftUI 视图(.onAppear、.onDisappear、.onTap)或手势(.onEnded、.onChanged、.updating)修改器,它们也可以被视为动作。当 URLSession 响应并返回我们可以解析为结构的 Data 时,这可以是一个成功的动作;但当响应为 404 或 JSONDecoder 不能理解有效载荷时,这也应成为一个失败的 Action。通知中心除了在整个系统中通知动作之外不做其他事情,例如键盘隐藏或设备旋转。CoreData和其他实时数据库具有通知当某些内容发生变化时的机制,这应该也成为动作。 动作与对应用程序相关的输入事件有关。 为了在您的应用程序中表示动作,您可以使用结构体、类或枚举,并以您认为更好的方式组织可能的动作列表。但是我们有一个推荐的方法,这将使您能够充分利用类型安全并避免出现问题,这种方法是通过使用枚举的关联值创建的树结构。 enum AppAction { case started case movie(MovieAction) case cast(CastAction) } enum MovieAction { case requestMovieList case gotMovieList(movies: [Movie]) case movieListResponseError(MovieResponseError) case selectMovie(id: UUID) } enum CastAction { case requestCastList(movieId: UUID) case gotCastList(movieId: UUID, cast: [Person]) case castListResponseError(CastResponseError) case selectPerson(id: UUID) } 您的应用程序中所有可能的事件都应该列在这些枚举中,并以您认为最相关的方式进行分组。在分组这些枚举时,要考虑的一个因素是模块化:如果需要,您可以将在不同框架中划分一些或所有这些枚举,以实现模块之间的严格界限,或者在不同的应用程序之间重用同一组动作。 例如,所有的应用程序都将有代表任何 iOS 应用程序生命周期的常见动作,如 willResignActive、didBecomeActive、didEnterBackground、willEnterForeground。如果多个应用程序需要了解此生命周期,也许创建一个代表特定领域的枚举会更方便。同样,对于网络可达性,我们应该考虑创建一个枚举来表示我们从系统收到的所有可能的事件,当我们的网络连接状态发生变化时,这可能被广泛应用于各种应用程序。 重要:由于 Swift 中的枚举没有与结构体相同的 KeyPath,我们强烈建议阅读Action Enum Properties 文档,并为每个情况实现属性,无论是手动还是使用代码生成器,这样您就可以避免编写大量的易出错的 switch/case 代码。我们还提供了一些模板来帮助您完成这项工作。 状态 状态表示应用程序在打开时的全部知识,通常在内存中并且是可变的。它与相关的输出属性有关。 SwiftRex 中没有“状态”协议或类型。但状态将会作为大多数核心数据结构的泛型参数被找到,这意味着您需要自行定义根状态类型。 从概念上讲,我们可以认为状态代表了应用在开启时持有的全部知识,通常是在内存中且可变的;它就像一张纸,你在上面写下一些值,对于你收到的每个行动,你都清除一个值并替换为另一个值。另一种思考状态的方法是功能编程:状态不持久化,但它是通过一个函数的结果,这个函数接收应用启动以来的所有初始条件以及接收到的所有行动,并通过应用所有行动改变计算当前值。这被称为事件溯源设计模式,并且最近在一些网络后端服务中变得流行,比如Kafka事件溯源。 在一个电池和内存有限的设备上,我们不能承受使用真实的事件溯源模式,因为这会太昂贵,每次请求一个简单的布尔值都必须重新构建应用的全部历史。因此,我们每次接收到一个行动时都会“缓存”新的状态,而这就是SwiftRex中所说的“状态”。所以,或许我们可以混合这两种思考状态的方式,找出哪种是对状态的一种更好的泛化。 状态是函数的结果,该函数接收两个参数:先前(或初始)状态和发生的一些行动,以确定新的状态。当越来越多的行动到来时,这会逐步发生。状态对于输送到用户的数据输出很有用。 然而,需要注意,有些东西看起来像是状态,但它们并不是。让我们假设你有一个向用户显示商品价格的 应用。这个价格在美国会显示为 "$3.00",在德国会为 "$3,00",或者这个产品可以以英镑列出来,所以在美国我们应该显示 "£3.00",在德国将会是 "£3,00"。在这个例子中,我们有 货币类型(£ 或 $) 数值类型(3) 区域设置(en_US 或 de_DE) 格式化字符串("$3.00"、"$3,00"、"£3.00" 或 "£3,00") 格式化字符串本身不是状态,因为它可以从其他属性中计算得出。这可以称为“派生状态”,而保存它则会导致不一致。我们必须记住在其中的任何一个更改时都要更新这个值。所以更好的做法是将这种String表示为计算属性或其他三个值的函数。这种派生状态的最好位置是在展示器或控制器中,除非计算它的成本很高,在这种情况下你可能将其存储在状态中,并且非常小心地处理它。幸运的是,正如我们将在Reducer部分看到的,SwiftRex可以帮助保持状态的一致性,不过,最好不要重复可以轻松且便宜计算的信息。 对于表示应用的状态,我们推荐使用值类型:结构体或枚举。元组也是可以接受的,但遗憾的是,Swift当前不允许我们让元组符合协议,而且我们通常希望我们的整个状态都是 Equatable 和有时是 Codable。 struct AppState: Equatable { var appLifecycle: AppLifecycle var movies: Loadable<[Movie]> = .neverLoaded var currentFilter: MovieFilter var selectedMovie: UUID? } struct MovieFilter: Equatable { var stringFilter: String? var yearMin: Int? var yearMax: Int? var ratingMin: Int? var ratingMax: Int? } enum AppLifecycle: Equatable { case backgroundActive case backgroundInactive case foregroundActive case foregroundInactive } enum Loadable<T> { case neverLoaded case loading case loaded(T) } extension Loadable: Equatable where T: Equatable {} 一些属性代表了状态机,例如,Loadable 枚举最终会从 .neverLoaded 变为 .loading,然后再到 .loaded([Movie]),在我们的 movies 属性中。学习何时以及如何以这种形状表示属性是一个更多实验SwiftRex并且逐步适应这种架构的过程。最终这会变得自然,你就可以开始编写自己的数据结构来表示这种状态机,这在无数情况下将非常有用。 将整个状态标注为可比较的,有助于我们在视模型未被使用的情况下减少UI更新,但这并非一个强烈的要求,还有其他方法可以实现这一点,尽管我们仍然推荐这样做。将状态标注为可编码的,对日志记录、调试、崩溃报告、监控等非常有用,如果可能的话,也推荐这样做。 核心部分 存储 存储类型 真实存储 商店投射 全部组合 中间件 泛型 返回IO和执行副作用 依赖注入 中间件示例 效应中间件 缩减器 存储(Store) 存储类型(StoreType) 这是一个协议,定义了“存储(Store)”的两个预期角色:接收/分发动作(ActionHandler);以及将当前应用的州变化(StateProvider)发布给可能的订阅者。它可以是一个真正的存储(如ReduxStoreBase)或只是一个代表真实存储的“代理”,例如,在StoreProjection的情况下。 存储类型是一个ActionHandler,这意味着参与者可以调度动作(通过ActionHandler/dispatch(_:)),这些动作将由该存储处理。这些动作最终将启动副作用或改变状态。这些动作也可以由副作用的结果触发,如API调用的回调,或CLLocation新的坐标。动作的处理方式将取决于StoreType的不同实现。 存储类型也是一个StateProvider,这意味着它了解某些状态,并且可以通过其发布者(通过StateProvider/statePublisher)通知可能的订阅者有关更改。如果这个StoreType拥有状态(单个真相来源)或只是从另一个存储代理它将取决于该协议的不同实现。 ┌──────────┐ │ UIButton │────────┐ └──────────┘ │ ┌───────────────────┐ │ dispatch<Action>(_ action: Action) │UIGestureRecognizer│───┼──────────────────────────────────────────────┐ └───────────────────┘ │ │ ┌───────────┐ │ ▼ │viewDidLoad│───────┘ ┏━━━━━━━━━━━━━━━━━━━━┓ └───────────┘ ┃ ┃░ ┃ ┃░ ┃ ┃░ ┌───────┐ ┃ ┃░ │UILabel│◀─ ─ ─ ─ ┐ ┃ ┃░ └───────┘ Combine, RxSwift ┌ ─ ─ ┻ ─ ┐ ┃░ │ or ReactiveSwift State Store ┃░ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│Publisher│ ┃░ ▼ │ subscribe(onNext:) ┃░ ┌─────────────┐ ▼ sink(receiveValue:) └ ─ ─ ┳ ─ ┘ ┃░ │ Diffable │ ┌─────────────┐ assign(to:on:) ┃ ┃░ │ DataSource │ │RxDataSources│ ┃ ┃░ └─────────────┘ └─────────────┘ ┃ ┃░ │ │ ┃ ┃░ ┌──────▼───────────────▼───────────┐ ┗━━━━━━━━━━━━━━━━━━━━┛░ │ │ ░░░░░░░░░░░░░░░░░░░░░░ │ │ │ │ │ │ │ UICollectionView │ │ │ │ │ │ │ │ │ └──────────────────────────────────┘ 有一些实现将是实际的存储,它是整个redux架构的中心枢纽的单一实例。其他实现可以是仅投影或主要存储,所以它们通过实现相同的角色来像存储一样行动,但不是直接拥有全局状态或处理动作,这些投影只应用链中的微小(纯)转换,并将其委托给真实的存储。这对于您想在自己的视图中拥有本地“存储”,但不希望它们复制数据或拥有任何类型的状态,而只想作为存储在幕后使用的情况非常有用。 有关真实商店的更多信息,请查阅 ReduxStoreBase 和 ReduxStoreProtocol,有关投影的更多信息,请查阅 StoreProjection 和 StoreType/projection(action:state:)。 什么是真实商店? 真实商店是一个在整个应用程序执行期间都需要创建和维持的类,因为它唯一的职责是作为单向数据流生命周期的协调者。这也是为什么我们只想有一个Store实例,所以你可以创建一个静态的单例实例,或者将其保存在您的AppDelegate中。如果您支持多个窗口并且希望在多个应用程序实例之间共享状态,请小心使用SceneDelegate。这通常是你想要的。这就是为什么建议您使用AppDelegate、单例或全局变量而不是SceneDelegate来保存Store。在SwiftUI中,您可以在应用程序协议中创建一个作为 Combine/StateObject 的Store。 @main struct MyApp: App { @StateObject var store = Store.createStore(dependencyInjection: World.default).asObservableViewModel(initialState: .initial) var body: some Scene { WindowGroup { ContentView(viewModel: ContentViewModel(store: store)) } } } SwiftRex将为创建自己的Store提供一个协议(ReduxStoreProtocol)和一个基类(ReduxStoreBase)以帮助您。 class Store: ReduxStoreBase<AppAction, AppState> { static func createStore(dependencyInjection: World) -> Store { let store = Store( subject: .combine(initialValue: .initial), reducer: AppModule.appReducer, middleware: AppModule.appMiddleware(dependencyInjection: dependencyInjection) emitsValue: .whenDifferent ) store.dispatch(AppAction.lifecycle(LifeCycleAction.start)) return store } } 什么是商店投影? 通常,您不希望视图能够访问整个应用程序状态或调度任何可能的全球应用程序操作。这不仅可能导致UI比所需的更频繁地刷新,还可能导致错误变得更多,将更复杂的代码放入视图层,并最终降低模块化,使视图与全局模型耦合在一起。 然而,您不希望将状态分割成多个部分,因为将它们保留在中心且唯一的点上可以确保一致性。同样,您不希望有多个地方分别处理动作,因为这可能会潜在的创建竞争条件。真实商店是全球状态唯一所有者,并有效处理动作的地方,这就是其应有的样子。 为了解决这两个问题,我们提供了一个按协议(StoreProjection)执行的操作,它遵守 StoreType 协议,因此从所有角度看都像一个真实的商店,但实际上它仅通过自定义类型使用状态和动作来投影真实商店,这是指你的模型的一部分(例如状态树中的分支),或者一个完全不同的实体,例如视图状态。一个 StoreProjection 有两个闭包,允许它在全局动作和视图使用的动作之间转换状态。这样,视图就不与全局模型耦合,而只是与其中的一小部分耦合,StoreProjection 中的闭包将负责提取/映射对视图有趣的部分。这样做也可以提高性能,因为视图只对全局状态中的相关属性进行刷新,而不是对任何属性。另一方面,视图可以调度有限的一组动作,这些动作将由 StoreProjection 中的闭包映射为全局动作。 可以从任何其他 StoreType 创建商店投影,甚至可以从另一个 StoreProjection 创建。这就像调用 StoreType/projection(action:state:) 并提供动作和状态映射闭包一样简单。 let storeProjection = store.projection( action: { viewAction in viewAction.toAppAction() } , state: { globalState in MyViewState.from(globalState: globalState) } ).asObservableViewModel(initialState: .empty) 总而言之 将所有内容组合起来,我们可能会有 @main struct MyApp: App { @StateObject var store = Store.createStore(dependencyInjection: World.default).asObservableViewModel(initialState: .initial) var body: some Scene { WindowGroup { ContentView( viewModel: store.projection( action: { (viewAction: ContentViewAction) -> AppAction? in viewAction.toAppAction() }, state: { (globalState: AppState) -> ContentViewState in ContentViewState.from(globalState: globalState) } ).asObservableViewModel(initialState: .empty) ) } } } struct ContentViewState: Equatable { let title: String static func from(globalState: AppState) -> ContentViewState { ContentViewState(title: "\(L10n.goodMorning), \(globalState.foo.bar.title)") } } enum ContentViewAction { case onAppear func toAppAction() -> AppAction? { switch self { case .onAppear: return AppAction.foo(.bar(.startTimer)) } } } 在上面的例子中,我们可以看到ContentView不知道全局模型,它只限于ContentViewAction和ContentViewState。它也只有在globalState.foo.bar.title变化时才会刷新,因为ContentViewState中没有映射其他属性,所以任何其他对AppState的更改都将被忽略。此外,ContentViewAction只有一个案例,即onAppear,这是视图可以分发的唯一事物,而不知道这最终将启动一个计时器(AppAction.foo(.bar(.startTimer)))。视图不应该知道领域逻辑,其动作应限于buttonTapped、onAppear、didScroll、toggle(enabled: Bool)以及其他只有UI交互含义的名称。如何将这些映射到App Actions是由其他部分负责的,在我们的例子中,是ContentViewAction本身,但它也可能是呈现器层、视图模型层,或者您决定创建以组织代码的任何结构。 这种做法也使得测试变得更容易,因为视图不持有任何逻辑,而投影转换是纯函数。 中间件 MiddlewareProtocol是一个插件,或几个插件的组合,它们被分配到应用的全局StoreType管道中,以处理收到的每个动作(InputActionType),并执行响应的副作用,并在过程中最终分派更多动作(OutputActionType)。在处理传入动作时,它还可以访问最新版本的StateType。 我们可以将中间件视为一个对象,它将动作转换为同步或异步任务,并在这些副作用完成后创建更多动作。同时,它还可以在处理动作时检查当前状态。 一个动作是一个轻量级结构,通常是一个枚举类型,它会分发给ActionHandler(通常是StoreType)。 例如,ReduxStoreProtocol将这些新到达的动作排队,并将其提交给中间件管道。换句话说,一个MiddlewareProtocol是处理动作的类,具有立即或异步任务回调后分派更多动作的能力。中间件也可以简单地忽略动作,或者它可以在响应中执行副作用,例如将日志记录到文件中或通过网络,或者执行HTTP请求。在异步任务的情况下,当它们完成时,中间件可以分派包含带有响应的有效负载的新动作(如JSON文件、电影数组、凭据等)。其他中间件将处理这个问题,或许甚至是将来的相同中间件,或者可能某个Reducer将使用这个动作来更改状态,因为MiddlewareProtocol本身不能更改状态,它只能读取它。 MiddlewareProtocol/handle(action:from:state:)函数将在Reducer之前被调用,所以如果你在那个时间点读取状态,它仍将是未更改的版本。在实现此函数时,你应该返回一个IO对象,这基本上是一个闭包,你可以在其中执行副作用和派发新的动作。在这个闭包内部,状态将是Reducers处理当前动作后的新值,所以如果你复制了旧状态,你可以比较它们、记录、审计、执行分析跟踪、遥测或与外部设备同步状态,例如苹果手表。通过网络的远程调试也是Middleware的一个很好的用途。 每个分派的动作都附带其动作源,即该动作的主要分发器。Middlewares可以访问文件名、行号、函数名以及有关负责创建和派发该动作的实体的其他信息,这是一份非常强大的调试信息,可以帮助开发者追踪信息在应用中的流动。 理想情况下,MiddlewareProtocol应该是一个小而可复用的盒子,仅处理一组非常有限的动作,并通过与其他小型中间件组合来创建更复杂的应用程序。例如,相同的CoreLocation中间件可以从iOS应用程序及其扩展、Apple Watch扩展,甚至是不同的应用程序中通用,只要它们共享某些子动作树和子状态结构即可。 一些中间件的建议 运行定时器,定期资源池或更新某些本地状态 订阅CoreData、Realm、Firebase Realtime Database或等效数据库的变化 成为CoreLocation代理,检查重大位置变化或信标范围,并触发更新状态的动作 成为HealthKit代理以跟踪活动,或者将其与CoreLocation观察结合起来以跟踪活动路径 记录器、遥测、审计、分析跟踪器、崩溃报告追踪点 监控或调试工具,如监控状态和动作的外部应用程序 WatchConnectivity同步,保持iOS和watchOS状态的同步 API调用和其他“冷可观察” 网络可达性 应用内的导航(Redux Coordinator模式) CoreBluetooth中心或外围经理 CoreNFC经理和代理 NotificationCenter和其他代理 WebSocket、TCP套接字、多点和许多其他连接协议 RxSwift可观察对象、ReactiveSwift信号生产者、Combine发布者 观察特征变化、设备旋转、语言/区域、深色模式、动态字体、后台/前台状态 任何副作用、I/O、网络、传感器、您想要抽象的第三方库 ┌────────┐ IO closure ┌─▶│ View 1 │ ┌─────┐ (don't run yet) ┌─────┐ │ └────────┘ │ │ handle ┌──────────┐ ┌───────────────────────────────────────▶│ │ send │ ┌────────┐ │ ├────────▶│Middleware│──┘ │ │────────────▶├─▶│ View 2 │ │ │ Action │ Pipeline │──┐ ┌─────┐ reduce ┌──────────┐ │ │ New state │ └────────┘ │ │ └──────────┘ └─▶│ │───────▶│ Reducer │──────────▶│ │ │ ┌────────┐ ┌──────┐ dispatch │ │ │Store│ Action │ Pipeline │ New state │ │ └─▶│ View 3 │ │Button│─────────▶│Store│ │ │ + └──────────┘ │Store│ └────────┘ └──────┘ Action │ │ └─────┘ State │ │ dispatch ┌─────┐ │ │ │ │ ┌─────────────────────────┐ New Action │ │ │ │ │ │─run──▶│ IO closure ├────────────▶│Store│─ ─ ▶ ... │ │ │ │ │ │ │ │ │ │ │ │ └─┬───────────────────────┘ └─────┘ └─────┘ └─────┘ │ ▲ request│ side-effects │side-effects ▼ response ┌ ─ ─ ─ ─ ─ │ External │─ ─ async ─ ─ ─ │ World ─ ─ ─ ─ ─ ┘ 泛型 中间件协议对3个关联类型进行泛型化 InputActionType: 此MiddlewareProtocol知道如何处理的活动类型,因此商店会将此类型的活动转发到该中间件。 中间件通常不需要处理整个全局动作树中所有可能的活动,因此我们可以决定允许它只关注动作的子集。 在这种情况下,动作类型可以是提升到全局动作类型以与其他中间件组合的子集。请参阅提升获取更多详细信息。 OutputActionType: 此 MiddlewareProtocol 将最终触发回商店以响应副作用的行为类型。这可以与 InputActionType 相同或不同,如果您想在请求和响应中分离枚举。 一般情况下,中间件不需要触发整个全局动作树的全部动作,因此我们可以决定只允许它触发动作子集,或者根本不触发任何动作,这样就可以安全地将 OutputActionType 设置为 Never。 在这种情况下,动作类型可以是提升到全局动作类型以与其他中间件组合的子集。请参阅提升获取更多详细信息。 StateType: 该 MiddlewareProtocol 为做出决策而需要读取的状态部分。在进行传入动作处理时,此中间件将能够从商店中读取最新的 StateType,但它永远不能写入或修改它。 大部分情况下,中间件不需要读取整个全局状态,因此我们可以决定只允许它读取状态子集,或者可能这个中间件不需要读取任何状态,因此可以将 StateType 安全地设置为 Void。 在这种情况下,此状态类型可以是一个子集,可以被提升到全局状态,以便与作用在全球状态上的其他中间件进行组合。请查阅有关提升的详细信息。 返回IO并执行副作用 在其最重要的功能中,中间件 MiddlewareProtocol/handle(action:from:state:) 需要返回一个 IO 对象,在此对象中执行副作用并可以分发新动作。 在某些情况下,我们可能希望不执行任何副作用或不执行 reductions 后的任何代码,在这种情况下,函数可以返回一个简单的 IO/pure()。 否则,返回一个接受 output(ActionHandler 类型的输出,接受 ActionHandler/dispatch(_:) 调用)闭包的闭包 public func handle(action: InputActionType, from dispatcher: ActionSource, state: @escaping GetState<StateType>) -> IO<OutputActionType> { if action != .myExpectedAction { return .pure() } return IO { output in output.dispatch(.showPopup) DispatchQueue.global().asyncAfter(.now() + .seconds(3)) { output.dispatch(.hidePopup) } } } 依赖注入 可测试性是软件开发时需要考虑的最重要方面之一。在 Redux 架构中,MiddlewareProtocol 是唯一允许执行副作用的对象类型,因此它是可测试性最具挑战性的地方。 为了提高可测试性,中间件应该尽可能少地使用外部依赖。如果它开始使用过多的外部依赖,考虑分割成较小的中间件,这将也有助于防止竞争条件和其他问题,将有助于测试,并使中间件更可重用。 此外,所有外部依赖都应该在初始化器中注入,这样在测试期间,您可以将其与模拟对象替换。如果您的中间件只使用到一个非常复杂对象的单个调用,而不是使用一个有许多函数要求的协议,请考虑创建一个只有一个函数要求的基本协议,或者甚至注入一个闭包,例如 @escaping (URLRequest) -> AnyPublisher<(Data, URLResponse), Error>。创建此类模拟将容易得多。 最后,考虑使用MiddlewareReader在依赖注入容器中包装这个中间件。 中间件示例 实现您的中间件时,您只需要处理传入的操作 public final class LoggerMiddleware: MiddlewareProtocol { public typealias InputActionType = AppGlobalAction // It wants to receive all possible app actions public typealias OutputActionType = Never // No action is generated from this Middleware public typealias StateType = AppGlobalState // It wants to read the whole app state private let logger: Logger private let now: () -> Date // Dependency Injection public init(logger: Logger = Logger.default, now: @escaping () -> Date) { self.logger = logger self.now = now } // inputAction: AppGlobalAction state: AppGlobalState output action: Never public func handle(action: InputActionType, from dispatcher: ActionSource, state: @escaping GetState<StateType>) -> IO<OutputActionType> { let stateBefore: AppGlobalState = state() let dateBefore = now() return IO { [weak self] output in guard let self = self else { return } let stateAfter = state() let dateAfter = self.now() let source = "\(dispatcher.file):\(dispatcher.line) - \(dispatcher.function) | \(dispatcher.info ?? "")" self.logger.log(action: action, from: source, before: stateBefore, after: stateAfter, dateBefore: dateBefore, dateAfter: dateAfter) } } } public final class FavoritesAPIMiddleware: MiddlewareProtocol { public typealias InputActionType = FavoritesAction // It wants to receive only actions related to Favorites public typealias OutputActionType = FavoritesAction // It wants to also dispatch actions related to Favorites public typealias StateType = FavoritesModel // It wants to read the app state that manages favorites private let api: API // Dependency Injection public init(api: API) { self.api = api } // inputAction: FavoritesAction state: FavoritesModel output action: FavoritesAction public func handle(action: InputActionType, from dispatcher: ActionSource, state: @escaping GetState<StateType>) -> IO<OutputActionType> { guard case let .toggleFavorite(movieId) = action else { return .pure() } let favoritesList = state() // state before reducer let makeFavorite = !favoritesList.contains(where: { $0.id == movieId }) return IO { [weak self] output in guard let self = self else { return } self.api.changeFavorite(id: movieId, makeFavorite: makeFavorite) (completion: { result in switch result { case let .success(value): output.dispatch(.changedFavorite(movieId, isFavorite: makeFavorite), info: "API.changeFavorite callback") case let .failure(error): output.dispatch(.changedFavoriteHasFailed(movieId, isFavorite: !makeFavorite, error: error), info: "api.changeFavorite callback") } }) } } } EffectMiddleware 这是一个旨在保持简单同时非常强大的中间件实现。对于每个传入的操作,您必须返回一个Effect,它只是根据您最喜欢的响应式库,对响应式可观察对象、发布者或SignalProducer进行包装。唯一条件是您的响应式流的输出(元素)必须是DispatchedAction,而错误必须是Never。DispatchedAction是一个具有操作自身和调度程序(操作源)的结构体,因此它对操作是通用的,并匹配EffectMiddleware的OutputAction。错误必须是Never,因为中间件预期解决所有副作用,包括错误。所以,如果您想处理错误,您可以在中间件中处理它;如果您想向用户警告错误,请在您的响应式流中捕获错误,将其转换为一个操作,如.somethingWentWrong(messageToTheUser: String)进行分发 并稍后将其缩减为AppState。 可选的EffectMiddleware还可以处理依赖项。这有助于将依赖注入中间件。如果依赖的泛型参数是Void,则中间件可以立即创建,无需传入任何依赖项,但是处理操作时不能使用任何外部依赖项。如果依赖的泛型参数具有一些类型或元组,则可以在处理操作时使用它们,但为了创建effect中间件,您需要提供该类型或元组。 重要:依赖项只能在Effect闭包内部可用,因为预期您“访问”外部世界仅在执行Effect时。 static let favouritesMiddleware = EffectMiddleware<FavoritesAction /* input action */, FavoritesAction /* output action */, AppState, FavouritesAPI /* dependencies */>.onAction { incomingAction, dispatcher, getState in switch incomingAction { case let .toggleFavorite(movieId): return Effect(token: "Any Hashable. Use this to cancel tasks, or to avoid two tasks of the same type") { context -> AnyPublisher<DispatchedAction<FavoritesAction>, Never> in let favoritesList = getState() let makeFavorite = !favoritesList.contains(where: { $0.id == movieId }) let api = context.dependencies return api.changeFavoritePublisher(id: movieId, makeFavorite: makeFavorite) .catch { error in DispatchedAction(.somethingWentWrong("Got an error: \(error)") } .eraseToAnyPublisher() } default: return .doNothing // Special type of Effect that, well, does nothing. } } Effect有一些有用的构造函数,如.doNothing、.fireAndForget、.just、.sequence、.promise、.toCancel等。此外,您可以将任何发布者、可观察对象或SignalProducer提升为Effect,只要它匹配所需的泛型参数,您可以使用.asEffect()函数。 Reducer Reducer是一个包裹在单例容器中的纯函数,它接受一个操作和当前状态来计算新状态。 MiddlewareProtocol管道可以执行两项操作:分发传出动作和处理传入动作。但他们不能做的是更改应用程序状态。中间件只能读取我们的应用的状态,但是当我们需要变异时,我们使用MutableReduceFunction函数。 (ActionType, inout StateType) -> Void 它与旧的ReduceFunction具有相同的语义(但性能更好) (ActionType, StateType) -> StateType 给定一个动作和当前状态(作为一个可变的输入输出),它计算新的状态并更改它 initial state is 42 action: increment reducer: increment 42 => new state 43 current state is 43 action: decrement reducer: decrement 43 => new state 42 current state is 42 action: half reducer: half 42 => new state 21 该函数正在将缓存的广告中的所有动作进行归约,并且它对于每个新的进入的动作进行增量归约。 理解这一点很重要,即reducer是一个同步操作,它在没有任何副作用(包括非显而易见的副作用,如创建Date()、使用DispatchQueue或Locale.current)的情况下计算新状态,所以永远不要向Reducer结构体中添加属性或调用任何外部函数。如果您倾向于这样做,请创建一个中间件,并通过它使用日期或地区分发动作。 还原器还负责保持状态的一致性,所以在更改状态之前始终进行最终的健康检查是个好主意,例如检查必须同时更改的其他依赖属性。 一旦reducer函数执行,存储将使用新的计算状态更新其单源,并将其传播到所有订阅者,他们将根据新状态反应并更新视图等。 此函数被包装在一个结构体中,以克服一些Swift限制,例如,允许我们将多个还原器组合成一个(单子操作,其中两个或多个还原器变成一个),或将还原器从局部类型提升到全局类型。 提升还原器的功能允许我们编写细粒度的“子还原器”,这些还原器将仅处理状态和/或动作的一部分,将其放置在不同框架和模块中,然后可以通过提供在全局和局部之间映射状态和动作的方法,将其插入更大的状态和动作处理程序中。有关更多信息,请参阅Lifting。 还原器的一个可能的实现是 let volumeReducer = Reducer<VolumeAction, VolumeState>.reduce { action, currentState in switch action { case .louder: currentState = VolumeState( isMute: false, // When increasing the volume, always unmute it. volume: min(100, currentState.volume + 5) ) case .quieter: currentState = VolumeState( isMute: currentState.isMute, volume: max(0, currentState.volume - 5) ) case .toggleMute: currentState = VolumeState( isMute: !currentState.isMute, volume: currentState.volume ) } } 请注意以下示例中的以下良好做法 没有DispatchQueue、线程、操作队列、承诺或响应式代码。 您需要实现此函数的所有内容都提供了action和currentState参数,不要使用来自全球范围的任何其他变量,甚至不用于读取。如果您需要其他东西,它应该要么在状态中,要么在动作有效载荷中。 不要启动副作用、请求、I/O、数据库调用。 在编写switch/case语句时避免使用default。这样,编译器可以帮助您更多。 尽可能使动作和状态泛型参数专一化。如果大状态是更大状态的一部分,不要诱使您将整个大状态传递给这个还原器。让它简短、简洁并具有针对性,这也帮助防止出现default情况或必须重新分配此还原器不会修改的属性。 ┌────────┐ IO closure ┌─▶│ View 1 │ ┌─────┐ (don't run yet) ┌─────┐ │ └────────┘ │ │ handle ┌──────────┐ ┌───────────────────────────────────────▶│ │ send │ ┌────────┐ │ ├────────▶│Middleware│──┘ │ │────────────▶├─▶│ View 2 │ │ │ Action │ Pipeline │──┐ ┌─────┐ reduce ┌──────────┐ │ │ New state │ └────────┘ │ │ └──────────┘ └─▶│ │───────▶│ Reducer │──────────▶│ │ │ ┌────────┐ ┌──────┐ dispatch │ │ │Store│ Action │ Pipeline │ New state │ │ └─▶│ View 3 │ │Button│─────────▶│Store│ │ │ + └──────────┘ │Store│ └────────┘ └──────┘ Action │ │ └─────┘ State │ │ dispatch ┌─────┐ │ │ │ │ ┌─────────────────────────┐ New Action │ │ │ │ │ │─run──▶│ IO closure ├────────────▶│Store│─ ─ ▶ ... │ │ │ │ │ │ │ │ │ │ │ │ └─┬───────────────────────┘ └─────┘ └─────┘ └─────┘ │ ▲ request│ side-effects │side-effects ▼ response ┌ ─ ─ ─ ─ ─ │ External │─ ─ async ─ ─ ─ │ World ─ ─ ─ ─ ─ ┘ 投影和提升 商店投射 提升 提升缩减器 使用闭包提升缩减器 使用KeyPath提升缩减器 提升中间件 使用闭包提升中间件 使用KeyPath提升中间件 可选转换 箭头的方向 使用KeyPaths 身份、忽略和荒谬 Xcode代码片段 存储投影 应用程序应该只有一个真实的“存储”,其中包含唯一的事实来源。然而,我们可以“派生”这个存储到称为存储分片的小子集中,这些小子集将处理状态或动作树的一部分,或者甚至完全不同的动作和状态,只要我们能将它们映射回原始存储类型。它不会存储任何内容,只投影原始存储。例如,视图可以定义完全定制的视图状态和视图动作,我们可以创建一个针对这些类型的StoreProjection,只要它支持真实的存储,并且类型可以以某种方式映射到视图状态和视图动作类型。存储分片将负责转换这些实体。 通常,您不希望视图能够访问整个应用程序状态或调度任何可能的全球应用程序操作。这不仅可能导致UI比所需的更频繁地刷新,还可能导致错误变得更多,将更复杂的代码放入视图层,并最终降低模块化,使视图与全局模型耦合在一起。 然而,您不希望将状态分割成多个部分,因为将它们保留在中心且唯一的点上可以确保一致性。同样,您不希望有多个地方分别处理动作,因为这可能会潜在的创建竞争条件。真实商店是全球状态唯一所有者,并有效处理动作的地方,这就是其应有的样子。 为了解决这两个问题,我们提供了一个按协议(StoreProjection)执行的操作,它遵守 StoreType 协议,因此从所有角度看都像一个真实的商店,但实际上它仅通过自定义类型使用状态和动作来投影真实商店,这是指你的模型的一部分(例如状态树中的分支),或者一个完全不同的实体,例如视图状态。一个 StoreProjection 有两个闭包,允许它在全局动作和视图使用的动作之间转换状态。这样,视图就不与全局模型耦合,而只是与其中的一小部分耦合,StoreProjection 中的闭包将负责提取/映射对视图有趣的部分。这样做也可以提高性能,因为视图只对全局状态中的相关属性进行刷新,而不是对任何属性。另一方面,视图可以调度有限的一组动作,这些动作将由 StoreProjection 中的闭包映射为全局动作。 可以从任何其他 StoreType 创建商店投影,甚至可以从另一个 StoreProjection 创建。这就像调用 StoreType/projection(action:state:) 并提供动作和状态映射闭包一样简单。 let storeProjection = store.projection( action: { viewAction in viewAction.toAppAction() } , state: { globalState in MyViewState.from(globalState: globalState) } ).asObservableViewModel(initialState: .empty) 有关真实存储和存储分片的更多信息,以及完整的代码示例,请参阅StoreType的文档。 提升 应用程序可能是一个复杂的产品,执行多个活动,而这些活动不一定相关。例如,同一个应用程序可能需要向天气API发出请求,使用CLLocation检查当前用户位置,并从NSUserDefaults中读取偏好设置。 尽管这些活动组合在一起以创建完整的体验,但可以将它们彼此隔离,以避免使用相同的资源,并导致竞争条件。同时,单独测试这些部分通常更容易,并导致更重要的测试。 理想情况下,我们应该组织我们的AppState和AppAction来考虑这些部分作为隔离的树。在上面的例子中,我们可以在AppState中有3个不同的属性,以及在我们的AppAction中有3个不同的枚举案例来分组与天气API、用户位置和NSUserDefaults访问相关的状态和动作。 如果我们根据模型中分组的3个路径将应用程序分为3种类型的Reducer和3种类型的MiddlewareProtocol,这将更有帮助。这些会非常有用。第一对Reducer和MiddlewareProtocol将是针对WeatherState和WeatherAction的通用的,第二对针对LocationState和LocationAction,第三对针对RepositoryState和RepositoryAction。它们甚至可以位于不同的框架中,这样编译器将阻止我们耦合Weather API代码与CLLocation代码,这是非常好的,因为这强制实行更好的做法并解锁代码重用。也许我们的CLLocation中继器/Reducer可以在完全不同的应用程序中检查公共交通路线时有用。 但最终,我们想将这些3种不同类型的实体放在一起,而我们的应用程序的StoreType“说话”的是AppAction和AppState,而不是由专门的处理器使用的子集。 enum AppAction { case weather(WeatherAction) case location(LocationAction) case repository(RepositoryAction) } struct AppState { let weather: WeatherState let location: LocationState let repository: RepositoryState } 对于针对WeatherAction和WeatherState的通用还原器,我们通过告诉还原器如何在全局树中找到所需属性,可以将它“提升”到全局类型AppAction和AppState。这将是\AppAction.weather和\AppState.weather。同样也可以为中间件,以及我们的应用程序中的其他2个还原器和中间件执行此操作。 当所有这些被提升到通用类型时,可以使用菱形运算符(<>)将它们组合在一起,并将其设置为存储处理程序。 重要:由于 Swift 中的枚举没有与结构体相同的 KeyPath,我们强烈建议阅读Action Enum Properties 文档,并为每个情况实现属性,无论是手动还是使用代码生成器,这样您就可以避免编写大量的易出错的 switch/case 代码。我们还提供了一些模板来帮助您完成这项工作。 让我们来探讨如何提升reducer和中间件。 提升Reducer Reducer有AppAction 输入、AppState 输入和AppState 输出,因为它只能处理动作(永远不会分散它们),读取状态和写入状态。 因此,提升方向应该是 Reducer: - ReducerAction? ← AppAction - ReducerState ←→ AppState 给出 // type 1 type 2 Reducer<ReducerAction, ReducerState> 转换 ╔═══════════════════╗ ║ ║ ╔═══════════════╗ ║ ║ ║ Reducer ║ .lift ║ Store ║ ╚═══════════════╝ ║ ║ │ ║ ║ ╚═══════════════════╝ │ │ │ │ ┌───────────┐ ┌─────┴─────┐ (AppAction) -> ReducerAction? │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ Reducer │ { $0.case?.reducerAction } │ │ Input Action │ Action │◀──────────────────────────────────────────────│ AppAction │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ KeyPath<AppAction, ReducerAction?> │ │ └─────┬─────┘ \AppAction.case?.reducerAction │ │ └───────────┘ │ │ │ get: (AppState) -> ReducerState │ { $0.reducerState } ┌───────────┐ ┌─────┴─────┐ set: (inout AppState, ReducerState) -> Void │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ Reducer │ { $0.reducerState = $1 } │ │ State │ State │◀─────────────────────────────────────────────▶│ AppState │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ WritableKeyPath<AppState, ReducerState> │ │ └─────┬─────┘ \AppState.reducerState │ │ └───────────┘ │ │ 使用闭包提升Reducer .lift( actionGetter: { (action: AppAction) -> ReducerAction? /* type 1 */ in // prism3 has associated value of ReducerAction, // and whole thing is Optional because Prism is always optional action.prism1?.prism2?.prism3 }, stateGetter: { (state: AppState) -> ReducerState /* type 2 */ in // property2: ReducerState state.property1.property2 }, stateSetter: { (state: inout AppState, newValue: ReducerState /* type 2 */) -> Void in // property2: ReducerState state.property1.property2 = newValue } ) 步骤 从头开始将Reducer的2个类型插入到3个闭包头中。 对于类型1,找到一个棱镜,从AppAction解析到匹配的类型。请确保已运行SOURCERY,并且所有枚举情况都由棱镜覆盖 对于状态获取闭包的类型2,找到从AppState解析到匹配类型的透镜(属性获取器)。 对于状态设置闭包的类型2,找到可以更改全局状态接收为新值(newValue)的透镜(属性设置器)。确保一切都可写。 使用KeyPath提升Reducer .lift( action: \AppAction.prism1?.prism2?.prism3, state: \AppState.property1.property2 ) 步骤 从上面的闭包示例开始 对于动作,我们可以使用来自\AppAction的KeyPath遍历棱镜树 对于状态,我们可以使用来自\AppState的可写KeyPath遍历属性,只要所有这些都声明为var,而不是let。 提升中间件 MiddlewareProtocol有AppAction 输入、AppAction 输出和AppState 输入,因为它可以处理动作,分散动作,并且只读状态( never 写它)。 因此,提升方向应该是 Middleware: - MiddlewareInputAction? ← AppAction - MiddlewareOutputAction → AppAction - MiddlewareState ← AppState 给出 // type 1 type 2 type 3 MyMiddleware<MiddlewareInputAction, MiddlewareOutputAction, MiddlewareState> 转换 ╔═══════════════════╗ ║ ║ ╔═══════════════╗ ║ ║ ║ Middleware ║ .lift ║ Store ║ ╚═══════════════╝ ║ ║ │ ║ ║ ╚═══════════════════╝ │ │ │ │ ┌───────────┐ ┌─────┴─────┐ (AppAction) -> MiddlewareInputAction? │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │Middleware │ { $0.case?.middlewareInputAction } │ │ Input Action │ Input │◀──────────────────────────────────────────────│ AppAction │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ Action │ KeyPath<AppAction, MiddlewareInputAction?> │ │ └─────┬─────┘ \AppAction.case?.middlewareInputAction │ │ └───────────┘ │ ┌─────┴─────┐ ┌───────────┐ (MiddlewareOutputAction) -> AppAction │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │Middleware │ { AppAction.case($0) } │ │ Output Action │ Output │──────────────────────────────────────────────▶│ AppAction │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ Action │ AppAction.case │ │ └───────────┘ │ │ │ └─────┬─────┘ ┌───────────┐ ┌─────┴─────┐ (AppState) -> MiddlewareState │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │Middleware │ { $0.middlewareState } │ │ State │ State │◀──────────────────────────────────────────────│ AppState │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ KeyPath<AppState, MiddlewareState> │ │ └─────┬─────┘ \AppState.middlewareState │ │ └───────────┘ │ │ 使用闭包提升中间件 .lift( inputAction: { (action: AppAction) -> MiddlewareInputAction? /* type 1 */ in // prism3 has associated value of MiddlewareInputAction, // and whole thing is Optional because Prism is always optional action.prism1?.prism2?.prism3 }, outputAction: { (local: MiddlewareOutputAction /* type 2 */) -> AppAction in // local is MiddlewareOutputAction, // an associated value for .prism3 AppAction.prism1(.prism2(.prism3(local))) }, state: { (state: AppState) -> MiddlewareState /* type 3 */ in // property2: MiddlewareState state.property1.property2 } ) 步骤 开始将 MyMiddleware 中的 3 种类型插入闭包头部。 对于类型1,找到一个棱镜,从AppAction解析到匹配的类型。请确保已运行SOURCERY,并且所有枚举情况都由棱镜覆盖 对于类型 2,从内部向外包装,直到达到 AppAction,在这个例子中,我们将它(即“它” = 本地)包装在 .prism3 中,然后包装在 .prism2 中,最后是 .prism1,最终到达 AppAction。 对于类型 3,找到从 AppState 解析到匹配类型的镜头(属性获取器)。 使用 KeyPath 提升中间件 .lift( inputAction: \AppAction.prism1?.prism2?.prism3, outputAction: Prism2.prism3, state: \AppState.property1.property2 ) .lift(outputAction: Prism1.prism2) .lift(outputAction: AppAction.prism1) 步骤 从上面的闭包示例开始 对于 inputAction,我们可以使用从 \AppAction 通过棱镜树遍历的 KeyPath 对于 outputAction 它不是一个 KeyPath,而是一个包装。因为我们不能同时包装超过一个层级,所以我们 为此使用闭包版本 逐级提升,从内部到外部,在这种情况下,遵循将本地包装到 Prism2(情况 .prism3)的步骤,然后将结果包装到 Prism1(情况 .prism2),最后将结果包装到 AppAction(情况 .prism1) 当只有一个层级时,没有必要担心 对于状态,我们可以使用从 \AppState 遍历属性的 KeyPath。 可选转换 如果某些动作正在通过存储器执行,一些减少器和中间件可能选择忽略它。例如,如果动作树与该中间件或减少器没有任何关联。这就是为什么,每个传入的动作(中间件的 InputAction 和还原器的简单动作)都是从 AppAction → Optional<Subset> 的转换。返回 nil 表示将忽略该动作。 对于其他方向,当中间件分发动作时,动作必须变为 AppAction,我们不能忽略中间件的话。 箭头方向 减少器 接收动作(输入动作)并且能够读写状态。 中间件 接收动作(输入动作),分发动作(输出动作)并且只读取状态(输入状态)。 在提升操作时,我们必须要记住这一点,因为它定义了变换的协变(covariant)/逆协变(contravariant),也就是所谓的映射或逆映射。 有一个特殊情况是对于reducer的State,因为那需要读写访问,换句话说,你被赋予了一个inout Whole和一个Part的新值,你使用这个新值来设置inout Whole中的正确路径。这正是WritableKeyPaths想要达成的目的,现在我们会更详细地说明。 KeyPaths的使用 KeyPath和Global -> Part转换是相同的,你给出以下方式的树描述:\Global.parent.part。 WritableKeyPath的用法语法类似,但更强大,允许我们转换(Global, Part) -> Global,或者(inout Global, Part) -> Void,两者是相同的。 但我们需要理解,只有当箭头的方向来自AppElement -> ReducerOrMiddlewareElement时,KeyPaths才是可能的,也就是说 Reducer: - ReducerAction? ← AppAction // Keypath is possible - ReducerState ←→ AppState // WritableKeyPath is possible Middleware: - MiddlewareInputAction? ← AppAction // KeyPath is possible - MiddlewareOutputAction → AppAction // NOT POSSIBLE - MiddlewareState ← AppState // KeyPath is possible 对于ReducerAction? ← AppAction和MiddlewareInputAction? ← AppAction,我们可以使用解析为Optional<ReducerOrMiddlewareAction>的KeyPaths { (globalAction: AppAction) -> ReducerOrMiddlewareAction? in globalAction.parent?.reducerOrMiddlewareAction } // or // KeyPath<AppAction, ReducerOrMiddlewareAction?> \AppAction.parent?.reducerOrMiddlewareAction 对于ReducerState ←→ AppState和MiddlewareState ← AppState转换,我们可以使用类似的语法,尽管reducer是inout(WritableKeyPath)。这意味着我们的整个树必须由var属性组成,而不是let。在这种情况下,除非Middleware或reducer接受Optional,否则转换不应该使用Optional。 { (globalState: AppState) -> PartState in globalState.something.thatsThePieceWeWant } { (globalState: inout AppState, newValue: PartState) -> Void in globalState.something.thatsThePieceWeWant = newValue } // or // KeyPath<AppState, PartState> or WritableKeyPath<AppState, PartState> \AppState.something.thatsThePieceWeWant // where: // var something // var thatsThePieceWeWant 对于MiddlewareOutputAction → AppAction,我们不能使用keypath,因为它没有意义,因为方向与我们的目标相反。在这种情况下,我们不是从全局值中展开/提取部分,而是从某个Middleware提供了特定操作,我们需要将其包装到AppAction中。这可以通过两种形式来实现 { (middlewareAction: MiddlewareAction) -> AppAction in AppAction.treeForMiddlewareAction(middlewareAction) } // or simply AppAction.treeForMiddlewareAction // please notice, not KeyPath, it doesn't start by \ 然而,短形式不能一次穿越两层 { (middlewareAction: MiddlewareAction) -> AppAction in AppAction.firstLevel( FirstLevel.secondLevel(middlewareAction) ) } // this will NOT compile (although a better Prism could solve that, probably): AppAction.firstLevel.secondLevel // You could try, however, to lift twice: .lift(outputAction: FirstLevel.secondLevel) // Notice that first we wrap the middleware value in the second level .lift(outputAction: AppAction.firstLevel) // And then we wrap the first level in the AppAction // The order must be from inside to outside, always. 同一性、忽略和荒谬 Void 当Middleware不需要State时,它可以是无(Void) 使用ignore提升Void,它相当于{ (_: Anything) -> Void in } Never 当Middleware不需要分发操作时,它可以是Never 使用absurd提升Never,它相当于{ (never: Never) -> Anything in } Identity 当某些部分的提升应该保持不变,因为它们已经处于期望的类型时 使用identity提升这些,它相当于{ $0 } 背后的理论:Void和Never是互为对偶 任何东西都可以成为Void(终端对象) Never(初始对象)可以成为任何东西 Void有1个可能的实例(它是单例) Never没有可能的实例 因为没有人能给你Never,你可以把Anything作为一种挑战。这就是为什么函数被称为ab(绝无可能)的原因,你不能调用它。 >Xcode Snippets // Reducer expanded .lift( actionGetter: { (action: AppAction) -> <#LocalAction#>? in action.<#something?.child#> }, stateGetter: { (state: AppState) -> <#LocalState#> in state.<#something.child#> }, stateSetter: { (state: inout AppState, newValue: <#LocalState#>) -> Void in state.<#something.child#> = newValue } ) // Reducer KeyPath: .lift( action: \AppAction.<#something?.child#>, state: \AppState.<#something.child#> ) // Middleware expanded .lift( inputAction: { (action: AppAction) -> <#LocalAction#>? in action.<#something?.child#> }, outputAction: { (local: <#LocalAction#>) -> AppAction in AppAction.<#something(.child(local))#> }, state: { (state: AppState) -> <#LocalState#> in state.<#something.child#> } ) // Middleware KeyPath .lift( inputAction: \AppAction.<#local#>, outputAction: AppAction.<#local#>, // not more than 1 level state: \AppState.<#local#> ) 架构 这种数据流在某种程度上是MVC的实现,与苹果的MVC有很大不同,它提供了一个非常严格和声明式的层责任描述,并通过更深入地定义其实施方式来强制Model层的增长:在这种情况下,Model是Store。所有Controller需要做的就是将视图操作转发给Store,并订阅状态变化,在必要时更新视图。如果这个流程听起来不像MVC,让我们看一下苹果网站上的截图 一个重要的区别是关于用户操作:在SwiftRex中,它由控制器转发并到达Store,因此更新状态的责任现在成为Store的责任。其余部分几乎一样,但是对Model的操作有更好的定义。 ╼━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╾ ╱░░░░░░░░░░░░░░░░░◉░░░░░░░░░░░░░░░░░░╲ ╱░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░╲ ┃░░░░░░░░░░░░░◉░░◖■■■■■■■◗░░░░░░░░░░░░░░░░░┃ ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃ ╭┃░╭━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╮░┃ │┃░┃ ┌──────────┐ ┃░┃ ╰┃░┃ │ UIButton │────────┐ ┃░┃ ┃░┃ └──────────┘ │ ┃░┃ ╭┃░┃ ┌───────────────────┐ │ ┃░┃╮ dispatch<Action>(_ action: Action) │┃░┃ │UIGestureRecognizer│───┼──────────────────────────────────────────────┐ │┃░┃ └───────────────────┘ │ ┃░┃│ │ ╰┃░┃ ┌───────────┐ │ ┃░┃│ ▼ ╭┃░┃ │viewDidLoad│───────┘ ┃░┃╯ ┏━━━━━━━━━━━━━━━━━━━━┓ │┃░┃ └───────────┘ ┃░┃ ┃ ┃░ │┃░┃ ┃░┃ ┃ ┃░ ╰┃░┃ ┃░┃ ┃ ┃░ ┃░┃ ┌───────┐ ┃░┃ ┃ ┃░ ┃░┃ │UILabel│◀─ ─ ─ ─ ┐ ┃░┃ ┃ ┃░ ┃░┃ └───────┘ ┃░┃ Combine, RxSwift ┌ ─ ─ ┻ ─ ┐ ┃░ ┃░┃ │ ┃░┃ or ReactiveSwift State Store ┃░ ┃░┃ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╋░─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│Publisher│ ┃░ ┃░┃ ▼ │ ┃░┃ subscribe(onNext:) ┃░ ┃░┃ ┌─────────────┐ ▼ ┃░┃ sink(receiveValue:) └ ─ ─ ┳ ─ ┘ ┃░ ┃░┃ │ Diffable │ ┌─────────────┐ ┃░┃ assign(to:on:) ┃ ┃░ ┃░┃ │ DataSource │ │RxDataSources│ ┃░┃ ┃ ┃░ ┃░┃ └─────────────┘ └─────────────┘ ┃░┃ ┃ ┃░ ┃░┃ │ │ ┃░┃ ┃ ┃░ ┃░┃ ┌──────▼───────────────▼───────────┐ ┃░┃ ┗━━━━━━━━━━━━━━━━━━━━┛░ ┃░┃ │ │ ┃░┃ ░░░░░░░░░░░░░░░░░░░░░░ ┃░┃ │ │ ┃░┃ ┃░┃ │ │ ┃░┃ ┃░┃ │ │ ┃░┃ ┃░┃ │ UICollectionView │ ┃░┃ ┃░┃ │ │ ┃░┃ ┃░┃ │ │ ┃░┃ ┃░┃ │ │ ┃░┃ ┃░┃ │ │ ┃░┃ ┃░┃ └──────────────────────────────────┘ ┃░┃ ┃░╰━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╯░┃ ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃ ┃░░░░░░░░░░░░░░░░░░░▓▓▓▓░░░░░░░░░░░░░░░░░░░┃ ┃░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░┃ ╲░░░░░░░░░░░░░░░░░░▓▓▓▓░░░░░░░░░░░░░░░░░░╱ ╲░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░╱ ╼━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╾ 您可以认为Store是一个非常重的“Model”层,完全独立于视图和控制器,其中所有业务逻辑都存在。乍一看,这可能看起来像是将“大量”问题从一个层面转移到了另一个层面,这就是为什么Store什么都不是,只是一种集合可组合盒子,具有非常明确的角色和,最重要的是,限制。 ╼━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╾ ╱░░░░░░░░░░░░░░░░░◉░░░░░░░░░░░░░░░░░░╲ ╱░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░╲ ┃░░░░░░░░░░░░░◉░░◖■■■■■■■◗░░░░░░░░░░░░░░░░░┃ ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃ ╭┃░╭━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╮░┃ │┃░┃ ┌────────┐ ┃░┃ ╰┃░┃ │ Button │────────┐ ┃░┃ ┃░┃ └────────┘ │ ┃░┃ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┏━━━━━━━━━━━━━━━━━━━━━━━┓ ╭┃░┃ ┌──────────────────┐ │ ┃░┃╮ dispatch ┃ ┃░ │┃░┃ │ Toggle │───┼────────────────────▶│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ │────────────▶┃ ┃░ │┃░┃ └──────────────────┘ │ ┃░┃│ view event f: (Event) → Action app action ┃ ┃░ ╰┃░┃ ┌──────────┐ │ ┃░┃│ │ │ ┃ ┃░ ╭┃░┃ │ onAppear │───────┘ ┃░┃╯ ┃ ┃░ │┃░┃ └──────────┘ ┃░┃ │ ObservableViewModel │ ┃ ┃░ │┃░┃ ┃░┃ ┃ ┃░ ╰┃░┃ ┃░┃ │ a projection of │ projection ┃ Store ┃░ ┃░┃ ┃░┃ the actual store ┃ ┃░ ┃░┃ ┃░┃ │ │ ┃ ┃░ ┃░┃ ┌────────────────────────┐ ┃░┃ ┃ ┃░ ┃░┃ │ │ ┃░┃ │ │ ┌┃─ ─ ─ ─ ─ ┐ ┃░ ┃░┃ │ @ObservedObject │◀ ─ ─ ╋░─ ─ ─ ─ ─ ─ ─ ─ ◀─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ◀─ ─ ─ ─ ─ ─ State ┃░ ┃░┃ │ │ ┃░┃ view state │ f: (State) → View │ app state │ Publisher │ ┃░ ┃░┃ └────────────────────────┘ ┃░┃ State ┳ ─ ─ ─ ─ ─ ┃░ ┃░┃ │ │ │ ┃░┃ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ┗━━━━━━━━━━━━━━━━━━━━━━━┛░ ┃░┃ ▼ ▼ ▼ ┃░┃ ░░░░░░░░░░░░░░░░░░░░░░░░░ ┃░┃ ┌────────┐ ┌────────┐ ┌────────┐ ┃░┃ ┃░┃ │ Text │ │ List │ │ForEach │ ┃░┃ ┃░┃ └────────┘ └────────┘ └────────┘ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░╰━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╯░┃ ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃ ┃░░░░░░░░░░░░░░░░░░░▓▓▓▓░░░░░░░░░░░░░░░░░░░┃ ┃░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░┃ ╲░░░░░░░░░░░░░░░░░░▓▓▓▓░░░░░░░░░░░░░░░░░░╱ ╲░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░╱ ╼━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╾ 那么SwiftUI怎么样?这种架构适合新的UI框架吗?事实上,在SwiftUI中,这种架构甚至工作得更好,因为SwiftUI受到几个函数式模式的影响,而且天生就是响应式和无状态的。在WWDC 2019期间提到多次,在SwiftUI中,“视图是状态的函数”,我们应始终追求单一的真理源,并且数据应始终单向流动。 安装 CocoaPods 在项目的根目录下创建或修改Podfile。您的设置将取决于您选择的ReactiveFramework。 对于Combine # Podfile source 'https://github.com/CocoaPods/Specs.git' use_frameworks! target 'MyAppTarget' do pod 'CombineRex' end 对于RxSwift # Podfile source 'https://github.com/CocoaPods/Specs.git' use_frameworks! target 'MyAppTarget' do pod 'RxSwiftRex' end 对于ReactiveSwift # Podfile source 'https://github.com/CocoaPods/Specs.git' use_frameworks! target 'MyAppTarget' do pod 'ReactiveSwiftRex' end 如上所述,有些行是可选的,因为最终的Podspecs已经包含了正确的依赖项。 然后,您只需安装Pod并打开.xcworkspace文件,而不是.xcodeproj文件。 $ pod install $ xed . Swift包管理器 在项目根目录中创建或修改Package.swift文件。您可以使用自动连接模式(静态/动态),或者使用后缀为Dynamic的项目,强制动态连接,从而克服目前Xcode在解决菱形依赖问题方面的局限性。 如果您只从一个目标使用它,自动模式应该就可以了。 组合,自动连接模式 // swift-tools-version:5.5 import PackageDescription let package = Package( name: "MyApp", platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], products: [ .executable(name: "MyApp", targets: ["MyApp"]) ], dependencies: [ .package(url: "https://github.com/SwiftRex/SwiftRex.git", from: "0.8.12") ], targets: [ .target(name: "MyApp", dependencies: [.product(name: "CombineRex", package: "SwiftRex")]) ] ) RxSwift,自动连接模式 // swift-tools-version:5.5 import PackageDescription let package = Package( name: "MyApp", platforms: [.macOS(.v10_10), .iOS(.v8), .tvOS(.v9), .watchOS(.v3)], products: [ .executable(name: "MyApp", targets: ["MyApp"]) ], dependencies: [ .package(url: "https://github.com/SwiftRex/SwiftRex.git", from: "0.8.12") ], targets: [ .target(name: "MyApp", dependencies: [.product(name: "RxSwiftRex", package: "SwiftRex")]) ] ) ReactiveSwift,自动连接模式 // swift-tools-version:5.5 import PackageDescription let package = Package( name: "MyApp", platforms: [.macOS(.v10_10), .iOS(.v8), .tvOS(.v9), .watchOS(.v3)], products: [ .executable(name: "MyApp", targets: ["MyApp"]) ], dependencies: [ .package(url: "https://github.com/SwiftRex/SwiftRex.git", from: "0.8.12") ], targets: [ .target(name: "MyApp", dependencies: [.product(name: "ReactiveSwiftRex", package: "SwiftRex")]) ] ) 组合,动态连接模式(类似地,对于RxSwift或ReactiveSwift产品,也使用添加"Dynamic"的方法) // swift-tools-version:5.5 import PackageDescription let package = Package( name: "MyApp", platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], products: [ .executable(name: "MyApp", targets: ["MyApp"]) ], dependencies: [ .package(url: "https://github.com/SwiftRex/SwiftRex.git", from: "0.8.12") ], targets: [ .target(name: "MyApp", dependencies: [.product(name: "CombineRexDynamic", package: "SwiftRex")]) ] ) 然后,您可以在终端上构建,或者使用现在原生支持SPM的Xcode 11或更高版本。 $ swift build $ xed . 重要:对于Xcode 12,请使用版本0.8.8。从版本0.9.0开始,需要Xcode 13。 Carthage 由于缺乏兴趣和高维护成本,Carthage不再受支持。 如果您认为这非常重要,请创建一个GitHub问题并告知我们,我们将评估恢复正常支持的可行性。在此期间,您可以检查最后一个兼容Carthage的版本,该版本为0.7.1,并最终将目标设置为该版本,直到我们提出更好的解决方案。
针对您最喜欢的响应式框架的单向数据流 如果您有任何关于 SwiftRex 或 Redux 以及通用编程的问题,请 SwiftRex 是一个框架,它结合了单向数据流架构和响应式编程(Combine、RxSwift 或 ReactiveSwift),为应用程序的全局状态提供中央 Store,SwiftUI 视图或 UIViewControllers 可观察、响应,还可派发来自用户交互的事件。 此模式也称为 "Redux",允许我们将应用程序视为一个单一的 纯函数,该函数接收用户事件作为输入,并根据 UI 更改返回。这种工作流程的优点将在不久的将来变得清晰。 API 文档可在以下位置找到. 在赶时间吗?已经熟悉其他 redux 实现? 没问题, 我们仍然建议阅读完整的 README, 如今,许多针对移动开发的架构和设计模式都旨在解决与《单一职责原则》(例如:庞大的 ViewController)相关的特定问题,或者提高可测试性和依赖管理。对于移动开发者而言,其他常见的挑战,如状态处理、竞态条件、模块化/组件化、线程安全性或正确处理 UI 生命周期和所有权等问题,虽然探讨较少,但同样可能对应用造成严重影响。 管理所有这些问题可能听起来是一项不可能完成的任务,需要众多模式和非常复杂的测试场景。毕竟,如何重现只有部分用户在设备上出现而开发者设备上从未发生的关键错误呢?这可能令人沮丧,我们中的大多数人可能都曾在某个时候遇到过这类问题。 这正是 SwiftRex 发光的地方,因为 强制执行单一职责原则的应用 [点击展开] 一些架构非常灵活,允许我们在任何地方添加任何代码片段。这对于仅由单人开发的小型应用来说可能没问题,但一旦项目和团队开始增长,某些层将变得非常大,承担过多的责任,隐式副作用、竞态条件和其他错误。在这种情况下,测试性也会受损,应用不同部分之间的统一性也会受损,因此查找和修复错误变得非常棘手。 SwiftRex 通过非常严格的代码放置政策来防止这种情况,政策通常由编译器强制执行。好吧,这听起来很困难且复杂,但实际上它比传统模式更容易,因为一旦您理解了这种架构,您就知道确切要做什么,您就知道基于其责任,具体要找到哪些代码行,您就知道如何测试每个组件,并且深刻理解每个层的边界。 为每一层提供清晰的测试策略(有关更多信息请参阅 TestingExtensions)[点击展开] 我们认为,架构不仅必须是可测试的,而且还应提供如何测试其每一层的明确指南。如果一个层只有一个任务,而且这个任务可以通过基于给定输入的期望输出断言来验证,那么测试将更有意义和广泛,因此当创建新功能时不会引入回归。 SwiftRex 架构中的大部分层将是纯函数,这意味着所有计算都仅从输入参数中进行,而且所有结果都将在输出中暴露,没有隐式效果或全局作用域的访问。此次测试无需模拟、存根、依赖注入或任何类型的准备,只需调用一个函数并与一个值一起使用,然后检查结果即可。 这对于 UI 层、展示层、reducers 和状态发布者都适用,因为整个链是由纯函数组成的。唯一需要依赖注入(因此需要模拟)的层是中间件,因为它是唯一依赖于服务并引发对外部世界副作用的一层。幸运的是,由于中间件是可组合的,我们可以将其拆分为非常小的独立部分,每个部分只做一项工作,这使得测试变得更加愉快和简单,因为您不必模拟数百个组件,只需注入一个即可。 我们还提供了 TestingExtensions,它允许我们使用 DSL 语法测试整个用例,该语法将验证所有 SwiftRex 层,确保没有意外副作用或动作发生,并且状态正如预期的那样逐步发生了变化。这是一种强大且有趣的方式来全面测试整个应用,只需简洁易写的几行代码。 将所有副作用封装在不可修改状态的组合/可重用中间件盒子中 [点击展开] 如果一个层需要在同一时间处理多个服务并异步响应用户请求,它们会更改状态,那么要保持这个状态的一致性和防止竞态条件就很困难。同时,也难以进行测试,因为一个效果可能会干扰另一个。 多年来,苹果和社区创建了用于访问网页、网络或设备传感器服务的一些非常棒的平台。不幸的是,其中一些框架依赖于代理模式,一些使用闭包/回调,一些使用通知中心、KVO或响应式流。这个各种通知形式的组合需要布尔标志、计数器和其他隐含状态,最终会因为竞态条件而破坏。 响应式框架有助于使这个过程更加统一和可组合,特别是在与其Cocoa扩展一起使用时。事实上,苹果也已经意识到了这一点,在2019年全球开发者大会(WWDC 2019)中,有很大一部分是专注于演示和解决这个问题,借助新引入的框架Combine和SwiftUI。 但将众多服务在响应式管道中进行组合并不总是容易,也有一些自己的陷阱,例如因一个流发出错误而导致整个管道取消,事件再入性,以及最后但同样重要的,掌握多个操作符的陡峭学习曲线。 SwiftRex大量使用了响应式编程,并且允许你根据自己的舒适度使用它。然而,我们也提供了一种更统一的方式来用1种数据类型和2个操作符组合不同的服务:中间件、`<>`操作符和`lift`操作符,所有其他操作都可以通过触发动作到自身、其他中间件或状态还原器来简化。尽管如此,你仍然可以选择创建更大的中间件并在传统的响应式流方式中处理多个源,如果你想这样做的话,但这可能对未经经验的开发者来说过于复杂,难以测试和在不同应用中重用。 由于这个主题非常广泛,最好在中间件文档中进行更详细的解释。 最小化对ViewControllers/ Presenters/ Interactors/ SwiftUI视图的依赖[点击展开] 浏览应用时传递依赖项始终是一个棘手的任务:ViewControllers的初始化器非常复杂,你必须始终考虑类是从NIB/XIB、编程或故事板创建的,然后你必须编写正确的初始化方法,不仅传入这个类所需要的所有依赖项,还要传入其子视图控制器和当你按下按钮时将要推入的下一个视图控制器所需要的依赖项。因此,你必须保持将成打的依赖项发送到你的视图,在你导航它们时。如果你不使用初始化器,而喜欢使用属性赋值,那么这些属性必须隐式解析,这并不是很好。 当然,协调器/线框模式对此有所帮助,但问题是某种方式你会将问题传送给路由器,这些路由器也需要保留他们实际上并未使用但下一个路由器将会使用的更多依赖项。你可以使用服务定位器模式,例如流行的环境方法,这实际上是一个处理问题的简单方法。但是,对这个单例进行测试可能很复杂,因为,好吧,它是一个单例。同时,有些人不喜欢隐式注入,他们更喜欢向层添加显式依赖项。 所以,解决这个问题并让每个人都满意是不可能的,对吗?其实并非如此。如果你的视图控制器只需要一个名为"Store"的单个依赖项,从中获取所需的状态并将所有用户事件分派到那里,而不实际执行任何工作,那么注入store将更容易完成,无论你是否使用显式注入或服务定位器。 好的,但仍然需要有人去做这些工作,这正是中间件执行的任务。在SwiftRex中,应该在应用入口处创建中间件,在配置并准备好依赖之后立即创建它们。然后创建所有中间件,注入它们执行工作所需的所有内容(希望每个中间件不超过2个依赖项,这样你知道它们并没有太多的责任)。最后,将它们组合起来,启动你的store。中间件可以有计时器,也可以纯粹地响应来自UI的动作,但它们是唯一具有副作用层面,因此也是唯一需要服务依赖层面的。 最后,您可以将地区、语言和界面特征添加到全局状态中,这样即使您需要在状态中创建数字和日期格式化器,也可以无需依赖注入完成,甚至在用户决定更改iOS设置时,仍能恰当地做出反应。 完全将状态、服务、变更和其他副作用从UI生命周期及其拥有树中分离出来 [点击展开] `UIViewControllers` 具有非常独特的所有权模型:你不能控制它。当它们位于导航堆栈中时、当显示标签或模态视图时,视图控制器被保留在内存中,但它们可以在任何时候释放,因此,放在视图控制器伞下的任何内容都可以被释放。我们一直使用并喜爱的 `[weak self]` 实际上有时可能是弱引用的,并且当我们“保护否则返回”时,很容易不为此事感到不安。无论视图是否显示,任何重要的任务都必须完成,因为你很容易关闭你的模态或弹出视图。虽然SwiftUI已经改善了这一点,但仍然可能从视图的闭包中启动异步任务,虽然现在这个视图是一个值类型,使这些错误变得更加困难,但仍然有可能。 SwiftRex通过强制所有和每一个副作用或异步任务都必须由中间件来完成,而不是视图来解决这个问题。而中间件的生命周期由store拥有,所以只要应用存在,我们就不会期望出现不幸的意外。 你仍然可以从你的视图中调度 "viewDidLoad"、"onAppear"、"onDisappear" 事件,以便执行任务取消,这样你得到的不是更少的而是在某种程度上增加了控制。更多信息请查看此链接 消除竞争条件 [点击展开] 当应用需要处理来自不同服务和来源的信息时,通常需要一个小的布尔标志来检查某事是否完成或失败。通常这是由于一些服务通过代理报告,一些通过闭包,还有其他一些创造性方式。使用标志同步这些多个来源,或者从并发任务中突变相同的变量或数组可能会导致非常奇怪的错误和崩溃,通常这些错误最难捕获、理解和修复。 处理锁和dispatch队列可以帮助解决这个问题,但反复以这种方式手动进行是单调的、危险的,必须编写考虑所有可能路径和时间的测试,其中一些测试最终会因为竞争条件仍然存在而变得不可靠。 通过强制所有应用程序的事件通过同一个队列,最终以一致的方式突变全局状态,SwiftRex可以防止竞态条件。首先,因为有中间件作为唯一来源的副作用和异步任务将简化对竞态条件的测试,特别是如果你让它们保持小规模,专注于单一任务。在这种情况下,你的响应将以FIFO队列的方式到来,并由所有reducer同时处理。其次,因为reducer是状态突变的大门守护者,使其无副作用对于成功和一致地突变至关重要。最后但并非最不重要的是,所有的事情都是在对动作的反应下发生的,并且可以轻松地将动作记录在日志中或放入您的事故报告中,包括谁调用了该动作,所以如果您仍发现竞态条件发生,您可以轻松地了解哪些动作正在突变状态以及这些动作从何而来。 允许更安全的类型编码风格 [点击展开] Swift泛型有点难学,还有关联类型协议。SwiftRex不需要你掌握泛型、了解协变或类型擦除,但你在这种感觉世界中越深入,你写的应用程序就越会通过编译器的验证而不是单元测试。将bug从运行时带到编译时是我们都应该接受的良好开发者的重要目标。可能更不好受一点的Swift类型系统,而不是在应用程序发布到野外后检查崩溃报告。这正好是Swift作为静态类型语言带来的心态,这种语言中,即使是nullability也是类型安全的,感谢Optional,我们现在可以安心地知道,除非我们不安全地并且明确地选择,否则我们不会访问空指针。 SwiftRex强制在所有地方使用强类型的事件/动作和状态:存储的行动派发器、中间件的动作处理器、中间件的动作输出、reducer的动作和状态输入输出以及最后存储的状态观察,整个流程是强类型的,因此编译器可以防止错误或运行时错误。 此外,中间件、reducer和存储都可以从部分状态和动作“提升”到全局状态和动作。这意味着你可以编写一个专门领域的强类型模块,例如网络可达性。你的中间件和reducer将“说话”网络领域状态和动作,例如是否连接、是Wi-Fi还是LTE、是否改变了网络连接动作等。然后你可以通过提供两个映射函数“提升”这两个组件——中间件和reducer——到你的应用程序的全局状态,一个用于提升状态,另一个用于提升动作。多亏了泛型,这个整个操作是完全类型安全的。同样可以通过“推断”主存储的存储投影来完成。存储投影实现了一个存储必须有的两个方法(输入动作和输出状态),但它不是真实的存储,而只是将全局状态和动作投影到一个更加本地化的领域,这意味着,视图事件转换成动作,视图状态转换成领域状态。 有了这些工具,我们相信,如果你愿意,你可以编写从边缘到边缘都是类型安全的的应用程序。 有助于实现模块化、组件化和项目之间的代码复用 [点击展开] 中间件应该专注于一个非常非常小的域,仅执行一种类型的工作,并以动作的形式返回。reducer应该专注于非常少的动作和状态的组合。视图应该能够访问非常小的状态部分,或者理想情况下能够访问一个视图状态,它是一个应用程序全局状态的平铺表示,使用直接映射到文本框字符串、切换的布尔值、进度条的double从0.0到1.0等原语。 然后,你可以把这三件——中间件、reducer、存储投影——提升到你的应用程序真正需要的全局状态和动作中。 SwiftRex 允许我们创建只有在需要时才能提升到全局域的微小工作单元,因此我们可以在非常具体的域中使用 Swift 框架,并用测试和 Playgrounds/SwiftUI 预览来使用而不必启动完整的 App。一旦此框架准备好,我们只需将其插入我们自己的 App,或者更好的是,多个 Apps。专注于小型域将解锁更好的抽象,并且当它从中间件(副作用)到视图时,你将拥有一个强大的工具来定义你的构建块。 强制实现唯一的真相源和正确的状态管理 [点击展开] SwiftRex 使得实现一个可信的、在屏幕之间永远不冲突或不同步的单一真相源成为可能。认为你的所有状态都在一个地方,一个包含一切的单一树,可能会有些可怕。一旦你将所有东西都聚集在一个地方,看到你需要多少状态可能会很吓人。但不用担心,这并不是你没有的东西,它已经在那里了,在 ViewController、在 Presenter、在控制服务结果的状态标志中,只是因为它太过分散,你看不到它在多大程度上。更糟糕的是,这将导致重复,因为当你需要从两个不同地方获取相同信息时,简单地复制并希望你能正确地保持它们同步要容易得多。 实际上,当你将整个 App 状态聚集到一个统一的树中时,你开始丢弃不再需要的很多东西,最终状态将比混乱的状态小。 正确编写全局状态和全局动作树可能会具有挑战性,但这确实是 App 域,推理这一点可能是工程师需要做的最重要的事情。 有关更多信息,请点击此链接 提供开发、测试和调试工具 [点击展开] 几个项目提供了 SwiftRex 工具,以帮助开发者在编写应用、测试、调试它或评估崩溃报告时。 CombineRextensions 提供 SwiftUI 扩展,用于与 CombineRex 一起工作,TestingExtensions 有“测试断言”,可以轻松愉快地解锁对用例的可测试性,InstrumentationMiddleware 允许你使用 Instruments 来查看 SwiftRex App 中发生的情况,SwiftRexMonitor 将成为Redux DevTools的Swift版本,你可以从外部 Mac 或 iOS 设备远程监控 App 的状态和动作,并注入动作来模拟副作用(例如,对 UI 测试很有用),GatedMiddleware 是一个中间件包装器,可以在运行时启用或禁用其他中间件,LoggerMiddleware 是一个非常强大的日志记录器,供开发者使用,可以轻松理解运行时发生的事情。更多中间件将被开源,例如,可以创建很好的 Crashlytics 报告,告诉你原来没有访问到的崩溃故事,并以这种方式,重建崩溃或用户报告。此外,提供代码生成工具(Sourcery 模板、Xcode 段落和模板、控制台工具),还有更高层次的 API,例如 EffectMiddlewares,它可以让我们像 Reducers 一样创建具有单个函数的中间件,以及 Handler,它允许将 Middlewares 和 Reducers 作为一个结构组在一起,以便能够同时提升它们。新依赖注入策略也将很快发布。 所有这些工具已经完成,并将在不久的将来发布,未来还有更多。 实话实说,这与编写应用程序的完全不同的方式,就像大多数反应式方法一样;但一旦你习惯了,这会让事情更有意义,并且使你能够在不同的项目之间重用更多的代码,为你提供更好的软件开发、测试、调试、日志记录工具,最后你能以前所未有的方式考虑事件、状态和突变。我保证,这将是一条不会有归途的道路,一次性的单向旅程。 反应式框架库 SwiftRex 当前支持3个主要的反应式框架 Apple Combine RxSwift ReactiveSwift 可以通过实现《ReactiveWrappers.swift》文件中可以找到的一些抽象桥梁,轻松地添加更多。为了防止向您的应用程序添加不必要的文件,SwiftRex分为4个包 SwiftRex:核心库 CombineRex:为Combine框架的实现 RxSwiftRex:为RxSwift框架的实现 ReactiveSwiftRex:为ReactiveSwift框架的实现 SwiftRex本身是不够的,所以你必须选择这三个实现中的一个。 部分 让我们通过将它们分为3个部分来了解SwiftRex的组件 概念部分 行为 状态 核心部分 存储 存储类型 真实存储 商店投射 全部组合 中间件 泛型 返回IO和执行副作用 依赖注入 中间件示例 效应中间件 缩减器 投射和提升 商店投射 提升 提升缩减器 使用闭包提升缩减器 使用KeyPath提升缩减器 提升中间件 使用闭包提升中间件 使用KeyPath提升中间件 可选转换 箭头的方向 使用KeyPaths 身份、忽略和荒谬 Xcode代码片段 概念部分 行为 状态 行为 动作表示外部(或有时内部)实体通知您的应用程序的事件。它涉及相关的输入事件。 SwiftRex 中没有“动作”协议或类型。然而,动作将会作为大多数核心数据结构的泛型参数被找到,这意味着您需要自行定义根动作类型。 从概念上讲,我们可以这样说,动作代表来自于您应用程序外部实体的某种发生的事情,这意味着用户的交互、定时器回调、Web服务的响应、位置服务的回调以及其他框架,一些内部实体也可以启动动作。例如,当 UIKit 结束加载您的视图时,我们可以将 viewDidLoad 看作一个动作,如果我们对这个事件感兴趣。对于 SwiftUI 视图(.onAppear、.onDisappear、.onTap)或手势(.onEnded、.onChanged、.updating)修改器,它们也可以被视为动作。当 URLSession 响应并返回我们可以解析为结构的 Data 时,这可以是一个成功的动作;但当响应为 404 或 JSONDecoder 不能理解有效载荷时,这也应成为一个失败的 Action。通知中心除了在整个系统中通知动作之外不做其他事情,例如键盘隐藏或设备旋转。CoreData和其他实时数据库具有通知当某些内容发生变化时的机制,这应该也成为动作。 动作与对应用程序相关的输入事件有关。 为了在您的应用程序中表示动作,您可以使用结构体、类或枚举,并以您认为更好的方式组织可能的动作列表。但是我们有一个推荐的方法,这将使您能够充分利用类型安全并避免出现问题,这种方法是通过使用枚举的关联值创建的树结构。 enum AppAction { case started case movie(MovieAction) case cast(CastAction) } enum MovieAction { case requestMovieList case gotMovieList(movies: [Movie]) case movieListResponseError(MovieResponseError) case selectMovie(id: UUID) } enum CastAction { case requestCastList(movieId: UUID) case gotCastList(movieId: UUID, cast: [Person]) case castListResponseError(CastResponseError) case selectPerson(id: UUID) } 您的应用程序中所有可能的事件都应该列在这些枚举中,并以您认为最相关的方式进行分组。在分组这些枚举时,要考虑的一个因素是模块化:如果需要,您可以将在不同框架中划分一些或所有这些枚举,以实现模块之间的严格界限,或者在不同的应用程序之间重用同一组动作。 例如,所有的应用程序都将有代表任何 iOS 应用程序生命周期的常见动作,如 willResignActive、didBecomeActive、didEnterBackground、willEnterForeground。如果多个应用程序需要了解此生命周期,也许创建一个代表特定领域的枚举会更方便。同样,对于网络可达性,我们应该考虑创建一个枚举来表示我们从系统收到的所有可能的事件,当我们的网络连接状态发生变化时,这可能被广泛应用于各种应用程序。 重要:由于 Swift 中的枚举没有与结构体相同的 KeyPath,我们强烈建议阅读Action Enum Properties 文档,并为每个情况实现属性,无论是手动还是使用代码生成器,这样您就可以避免编写大量的易出错的 switch/case 代码。我们还提供了一些模板来帮助您完成这项工作。 状态 状态表示应用程序在打开时的全部知识,通常在内存中并且是可变的。它与相关的输出属性有关。 SwiftRex 中没有“状态”协议或类型。但状态将会作为大多数核心数据结构的泛型参数被找到,这意味着您需要自行定义根状态类型。 从概念上讲,我们可以认为状态代表了应用在开启时持有的全部知识,通常是在内存中且可变的;它就像一张纸,你在上面写下一些值,对于你收到的每个行动,你都清除一个值并替换为另一个值。另一种思考状态的方法是功能编程:状态不持久化,但它是通过一个函数的结果,这个函数接收应用启动以来的所有初始条件以及接收到的所有行动,并通过应用所有行动改变计算当前值。这被称为事件溯源设计模式,并且最近在一些网络后端服务中变得流行,比如Kafka事件溯源。 在一个电池和内存有限的设备上,我们不能承受使用真实的事件溯源模式,因为这会太昂贵,每次请求一个简单的布尔值都必须重新构建应用的全部历史。因此,我们每次接收到一个行动时都会“缓存”新的状态,而这就是SwiftRex中所说的“状态”。所以,或许我们可以混合这两种思考状态的方式,找出哪种是对状态的一种更好的泛化。 状态是函数的结果,该函数接收两个参数:先前(或初始)状态和发生的一些行动,以确定新的状态。当越来越多的行动到来时,这会逐步发生。状态对于输送到用户的数据输出很有用。 然而,需要注意,有些东西看起来像是状态,但它们并不是。让我们假设你有一个向用户显示商品价格的 应用。这个价格在美国会显示为 "$3.00",在德国会为 "$3,00",或者这个产品可以以英镑列出来,所以在美国我们应该显示 "£3.00",在德国将会是 "£3,00"。在这个例子中,我们有 货币类型(£ 或 $) 数值类型(3) 区域设置(en_US 或 de_DE) 格式化字符串("$3.00"、"$3,00"、"£3.00" 或 "£3,00") 格式化字符串本身不是状态,因为它可以从其他属性中计算得出。这可以称为“派生状态”,而保存它则会导致不一致。我们必须记住在其中的任何一个更改时都要更新这个值。所以更好的做法是将这种String表示为计算属性或其他三个值的函数。这种派生状态的最好位置是在展示器或控制器中,除非计算它的成本很高,在这种情况下你可能将其存储在状态中,并且非常小心地处理它。幸运的是,正如我们将在Reducer部分看到的,SwiftRex可以帮助保持状态的一致性,不过,最好不要重复可以轻松且便宜计算的信息。 对于表示应用的状态,我们推荐使用值类型:结构体或枚举。元组也是可以接受的,但遗憾的是,Swift当前不允许我们让元组符合协议,而且我们通常希望我们的整个状态都是 Equatable 和有时是 Codable。 struct AppState: Equatable { var appLifecycle: AppLifecycle var movies: Loadable<[Movie]> = .neverLoaded var currentFilter: MovieFilter var selectedMovie: UUID? } struct MovieFilter: Equatable { var stringFilter: String? var yearMin: Int? var yearMax: Int? var ratingMin: Int? var ratingMax: Int? } enum AppLifecycle: Equatable { case backgroundActive case backgroundInactive case foregroundActive case foregroundInactive } enum Loadable<T> { case neverLoaded case loading case loaded(T) } extension Loadable: Equatable where T: Equatable {} 一些属性代表了状态机,例如,Loadable 枚举最终会从 .neverLoaded 变为 .loading,然后再到 .loaded([Movie]),在我们的 movies 属性中。学习何时以及如何以这种形状表示属性是一个更多实验SwiftRex并且逐步适应这种架构的过程。最终这会变得自然,你就可以开始编写自己的数据结构来表示这种状态机,这在无数情况下将非常有用。 将整个状态标注为可比较的,有助于我们在视模型未被使用的情况下减少UI更新,但这并非一个强烈的要求,还有其他方法可以实现这一点,尽管我们仍然推荐这样做。将状态标注为可编码的,对日志记录、调试、崩溃报告、监控等非常有用,如果可能的话,也推荐这样做。 核心部分 存储 存储类型 真实存储 商店投射 全部组合 中间件 泛型 返回IO和执行副作用 依赖注入 中间件示例 效应中间件 缩减器 存储(Store) 存储类型(StoreType) 这是一个协议,定义了“存储(Store)”的两个预期角色:接收/分发动作(ActionHandler);以及将当前应用的州变化(StateProvider)发布给可能的订阅者。它可以是一个真正的存储(如ReduxStoreBase)或只是一个代表真实存储的“代理”,例如,在StoreProjection的情况下。 存储类型是一个ActionHandler,这意味着参与者可以调度动作(通过ActionHandler/dispatch(_:)),这些动作将由该存储处理。这些动作最终将启动副作用或改变状态。这些动作也可以由副作用的结果触发,如API调用的回调,或CLLocation新的坐标。动作的处理方式将取决于StoreType的不同实现。 存储类型也是一个StateProvider,这意味着它了解某些状态,并且可以通过其发布者(通过StateProvider/statePublisher)通知可能的订阅者有关更改。如果这个StoreType拥有状态(单个真相来源)或只是从另一个存储代理它将取决于该协议的不同实现。 ┌──────────┐ │ UIButton │────────┐ └──────────┘ │ ┌───────────────────┐ │ dispatch<Action>(_ action: Action) │UIGestureRecognizer│───┼──────────────────────────────────────────────┐ └───────────────────┘ │ │ ┌───────────┐ │ ▼ │viewDidLoad│───────┘ ┏━━━━━━━━━━━━━━━━━━━━┓ └───────────┘ ┃ ┃░ ┃ ┃░ ┃ ┃░ ┌───────┐ ┃ ┃░ │UILabel│◀─ ─ ─ ─ ┐ ┃ ┃░ └───────┘ Combine, RxSwift ┌ ─ ─ ┻ ─ ┐ ┃░ │ or ReactiveSwift State Store ┃░ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│Publisher│ ┃░ ▼ │ subscribe(onNext:) ┃░ ┌─────────────┐ ▼ sink(receiveValue:) └ ─ ─ ┳ ─ ┘ ┃░ │ Diffable │ ┌─────────────┐ assign(to:on:) ┃ ┃░ │ DataSource │ │RxDataSources│ ┃ ┃░ └─────────────┘ └─────────────┘ ┃ ┃░ │ │ ┃ ┃░ ┌──────▼───────────────▼───────────┐ ┗━━━━━━━━━━━━━━━━━━━━┛░ │ │ ░░░░░░░░░░░░░░░░░░░░░░ │ │ │ │ │ │ │ UICollectionView │ │ │ │ │ │ │ │ │ └──────────────────────────────────┘ 有一些实现将是实际的存储,它是整个redux架构的中心枢纽的单一实例。其他实现可以是仅投影或主要存储,所以它们通过实现相同的角色来像存储一样行动,但不是直接拥有全局状态或处理动作,这些投影只应用链中的微小(纯)转换,并将其委托给真实的存储。这对于您想在自己的视图中拥有本地“存储”,但不希望它们复制数据或拥有任何类型的状态,而只想作为存储在幕后使用的情况非常有用。 有关真实商店的更多信息,请查阅 ReduxStoreBase 和 ReduxStoreProtocol,有关投影的更多信息,请查阅 StoreProjection 和 StoreType/projection(action:state:)。 什么是真实商店? 真实商店是一个在整个应用程序执行期间都需要创建和维持的类,因为它唯一的职责是作为单向数据流生命周期的协调者。这也是为什么我们只想有一个Store实例,所以你可以创建一个静态的单例实例,或者将其保存在您的AppDelegate中。如果您支持多个窗口并且希望在多个应用程序实例之间共享状态,请小心使用SceneDelegate。这通常是你想要的。这就是为什么建议您使用AppDelegate、单例或全局变量而不是SceneDelegate来保存Store。在SwiftUI中,您可以在应用程序协议中创建一个作为 Combine/StateObject 的Store。 @main struct MyApp: App { @StateObject var store = Store.createStore(dependencyInjection: World.default).asObservableViewModel(initialState: .initial) var body: some Scene { WindowGroup { ContentView(viewModel: ContentViewModel(store: store)) } } } SwiftRex将为创建自己的Store提供一个协议(ReduxStoreProtocol)和一个基类(ReduxStoreBase)以帮助您。 class Store: ReduxStoreBase<AppAction, AppState> { static func createStore(dependencyInjection: World) -> Store { let store = Store( subject: .combine(initialValue: .initial), reducer: AppModule.appReducer, middleware: AppModule.appMiddleware(dependencyInjection: dependencyInjection) emitsValue: .whenDifferent ) store.dispatch(AppAction.lifecycle(LifeCycleAction.start)) return store } } 什么是商店投影? 通常,您不希望视图能够访问整个应用程序状态或调度任何可能的全球应用程序操作。这不仅可能导致UI比所需的更频繁地刷新,还可能导致错误变得更多,将更复杂的代码放入视图层,并最终降低模块化,使视图与全局模型耦合在一起。 然而,您不希望将状态分割成多个部分,因为将它们保留在中心且唯一的点上可以确保一致性。同样,您不希望有多个地方分别处理动作,因为这可能会潜在的创建竞争条件。真实商店是全球状态唯一所有者,并有效处理动作的地方,这就是其应有的样子。 为了解决这两个问题,我们提供了一个按协议(StoreProjection)执行的操作,它遵守 StoreType 协议,因此从所有角度看都像一个真实的商店,但实际上它仅通过自定义类型使用状态和动作来投影真实商店,这是指你的模型的一部分(例如状态树中的分支),或者一个完全不同的实体,例如视图状态。一个 StoreProjection 有两个闭包,允许它在全局动作和视图使用的动作之间转换状态。这样,视图就不与全局模型耦合,而只是与其中的一小部分耦合,StoreProjection 中的闭包将负责提取/映射对视图有趣的部分。这样做也可以提高性能,因为视图只对全局状态中的相关属性进行刷新,而不是对任何属性。另一方面,视图可以调度有限的一组动作,这些动作将由 StoreProjection 中的闭包映射为全局动作。 可以从任何其他 StoreType 创建商店投影,甚至可以从另一个 StoreProjection 创建。这就像调用 StoreType/projection(action:state:) 并提供动作和状态映射闭包一样简单。 let storeProjection = store.projection( action: { viewAction in viewAction.toAppAction() } , state: { globalState in MyViewState.from(globalState: globalState) } ).asObservableViewModel(initialState: .empty) 总而言之 将所有内容组合起来,我们可能会有 @main struct MyApp: App { @StateObject var store = Store.createStore(dependencyInjection: World.default).asObservableViewModel(initialState: .initial) var body: some Scene { WindowGroup { ContentView( viewModel: store.projection( action: { (viewAction: ContentViewAction) -> AppAction? in viewAction.toAppAction() }, state: { (globalState: AppState) -> ContentViewState in ContentViewState.from(globalState: globalState) } ).asObservableViewModel(initialState: .empty) ) } } } struct ContentViewState: Equatable { let title: String static func from(globalState: AppState) -> ContentViewState { ContentViewState(title: "\(L10n.goodMorning), \(globalState.foo.bar.title)") } } enum ContentViewAction { case onAppear func toAppAction() -> AppAction? { switch self { case .onAppear: return AppAction.foo(.bar(.startTimer)) } } } 在上面的例子中,我们可以看到ContentView不知道全局模型,它只限于ContentViewAction和ContentViewState。它也只有在globalState.foo.bar.title变化时才会刷新,因为ContentViewState中没有映射其他属性,所以任何其他对AppState的更改都将被忽略。此外,ContentViewAction只有一个案例,即onAppear,这是视图可以分发的唯一事物,而不知道这最终将启动一个计时器(AppAction.foo(.bar(.startTimer)))。视图不应该知道领域逻辑,其动作应限于buttonTapped、onAppear、didScroll、toggle(enabled: Bool)以及其他只有UI交互含义的名称。如何将这些映射到App Actions是由其他部分负责的,在我们的例子中,是ContentViewAction本身,但它也可能是呈现器层、视图模型层,或者您决定创建以组织代码的任何结构。 这种做法也使得测试变得更容易,因为视图不持有任何逻辑,而投影转换是纯函数。 中间件 MiddlewareProtocol是一个插件,或几个插件的组合,它们被分配到应用的全局StoreType管道中,以处理收到的每个动作(InputActionType),并执行响应的副作用,并在过程中最终分派更多动作(OutputActionType)。在处理传入动作时,它还可以访问最新版本的StateType。 我们可以将中间件视为一个对象,它将动作转换为同步或异步任务,并在这些副作用完成后创建更多动作。同时,它还可以在处理动作时检查当前状态。 一个动作是一个轻量级结构,通常是一个枚举类型,它会分发给ActionHandler(通常是StoreType)。 例如,ReduxStoreProtocol将这些新到达的动作排队,并将其提交给中间件管道。换句话说,一个MiddlewareProtocol是处理动作的类,具有立即或异步任务回调后分派更多动作的能力。中间件也可以简单地忽略动作,或者它可以在响应中执行副作用,例如将日志记录到文件中或通过网络,或者执行HTTP请求。在异步任务的情况下,当它们完成时,中间件可以分派包含带有响应的有效负载的新动作(如JSON文件、电影数组、凭据等)。其他中间件将处理这个问题,或许甚至是将来的相同中间件,或者可能某个Reducer将使用这个动作来更改状态,因为MiddlewareProtocol本身不能更改状态,它只能读取它。 MiddlewareProtocol/handle(action:from:state:)函数将在Reducer之前被调用,所以如果你在那个时间点读取状态,它仍将是未更改的版本。在实现此函数时,你应该返回一个IO对象,这基本上是一个闭包,你可以在其中执行副作用和派发新的动作。在这个闭包内部,状态将是Reducers处理当前动作后的新值,所以如果你复制了旧状态,你可以比较它们、记录、审计、执行分析跟踪、遥测或与外部设备同步状态,例如苹果手表。通过网络的远程调试也是Middleware的一个很好的用途。 每个分派的动作都附带其动作源,即该动作的主要分发器。Middlewares可以访问文件名、行号、函数名以及有关负责创建和派发该动作的实体的其他信息,这是一份非常强大的调试信息,可以帮助开发者追踪信息在应用中的流动。 理想情况下,MiddlewareProtocol应该是一个小而可复用的盒子,仅处理一组非常有限的动作,并通过与其他小型中间件组合来创建更复杂的应用程序。例如,相同的CoreLocation中间件可以从iOS应用程序及其扩展、Apple Watch扩展,甚至是不同的应用程序中通用,只要它们共享某些子动作树和子状态结构即可。 一些中间件的建议 运行定时器,定期资源池或更新某些本地状态 订阅CoreData、Realm、Firebase Realtime Database或等效数据库的变化 成为CoreLocation代理,检查重大位置变化或信标范围,并触发更新状态的动作 成为HealthKit代理以跟踪活动,或者将其与CoreLocation观察结合起来以跟踪活动路径 记录器、遥测、审计、分析跟踪器、崩溃报告追踪点 监控或调试工具,如监控状态和动作的外部应用程序 WatchConnectivity同步,保持iOS和watchOS状态的同步 API调用和其他“冷可观察” 网络可达性 应用内的导航(Redux Coordinator模式) CoreBluetooth中心或外围经理 CoreNFC经理和代理 NotificationCenter和其他代理 WebSocket、TCP套接字、多点和许多其他连接协议 RxSwift可观察对象、ReactiveSwift信号生产者、Combine发布者 观察特征变化、设备旋转、语言/区域、深色模式、动态字体、后台/前台状态 任何副作用、I/O、网络、传感器、您想要抽象的第三方库 ┌────────┐ IO closure ┌─▶│ View 1 │ ┌─────┐ (don't run yet) ┌─────┐ │ └────────┘ │ │ handle ┌──────────┐ ┌───────────────────────────────────────▶│ │ send │ ┌────────┐ │ ├────────▶│Middleware│──┘ │ │────────────▶├─▶│ View 2 │ │ │ Action │ Pipeline │──┐ ┌─────┐ reduce ┌──────────┐ │ │ New state │ └────────┘ │ │ └──────────┘ └─▶│ │───────▶│ Reducer │──────────▶│ │ │ ┌────────┐ ┌──────┐ dispatch │ │ │Store│ Action │ Pipeline │ New state │ │ └─▶│ View 3 │ │Button│─────────▶│Store│ │ │ + └──────────┘ │Store│ └────────┘ └──────┘ Action │ │ └─────┘ State │ │ dispatch ┌─────┐ │ │ │ │ ┌─────────────────────────┐ New Action │ │ │ │ │ │─run──▶│ IO closure ├────────────▶│Store│─ ─ ▶ ... │ │ │ │ │ │ │ │ │ │ │ │ └─┬───────────────────────┘ └─────┘ └─────┘ └─────┘ │ ▲ request│ side-effects │side-effects ▼ response ┌ ─ ─ ─ ─ ─ │ External │─ ─ async ─ ─ ─ │ World ─ ─ ─ ─ ─ ┘ 泛型 中间件协议对3个关联类型进行泛型化 InputActionType: 此MiddlewareProtocol知道如何处理的活动类型,因此商店会将此类型的活动转发到该中间件。 中间件通常不需要处理整个全局动作树中所有可能的活动,因此我们可以决定允许它只关注动作的子集。 在这种情况下,动作类型可以是提升到全局动作类型以与其他中间件组合的子集。请参阅提升获取更多详细信息。 OutputActionType: 此 MiddlewareProtocol 将最终触发回商店以响应副作用的行为类型。这可以与 InputActionType 相同或不同,如果您想在请求和响应中分离枚举。 一般情况下,中间件不需要触发整个全局动作树的全部动作,因此我们可以决定只允许它触发动作子集,或者根本不触发任何动作,这样就可以安全地将 OutputActionType 设置为 Never。 在这种情况下,动作类型可以是提升到全局动作类型以与其他中间件组合的子集。请参阅提升获取更多详细信息。 StateType: 该 MiddlewareProtocol 为做出决策而需要读取的状态部分。在进行传入动作处理时,此中间件将能够从商店中读取最新的 StateType,但它永远不能写入或修改它。 大部分情况下,中间件不需要读取整个全局状态,因此我们可以决定只允许它读取状态子集,或者可能这个中间件不需要读取任何状态,因此可以将 StateType 安全地设置为 Void。 在这种情况下,此状态类型可以是一个子集,可以被提升到全局状态,以便与作用在全球状态上的其他中间件进行组合。请查阅有关提升的详细信息。 返回IO并执行副作用 在其最重要的功能中,中间件 MiddlewareProtocol/handle(action:from:state:) 需要返回一个 IO 对象,在此对象中执行副作用并可以分发新动作。 在某些情况下,我们可能希望不执行任何副作用或不执行 reductions 后的任何代码,在这种情况下,函数可以返回一个简单的 IO/pure()。 否则,返回一个接受 output(ActionHandler 类型的输出,接受 ActionHandler/dispatch(_:) 调用)闭包的闭包 public func handle(action: InputActionType, from dispatcher: ActionSource, state: @escaping GetState<StateType>) -> IO<OutputActionType> { if action != .myExpectedAction { return .pure() } return IO { output in output.dispatch(.showPopup) DispatchQueue.global().asyncAfter(.now() + .seconds(3)) { output.dispatch(.hidePopup) } } } 依赖注入 可测试性是软件开发时需要考虑的最重要方面之一。在 Redux 架构中,MiddlewareProtocol 是唯一允许执行副作用的对象类型,因此它是可测试性最具挑战性的地方。 为了提高可测试性,中间件应该尽可能少地使用外部依赖。如果它开始使用过多的外部依赖,考虑分割成较小的中间件,这将也有助于防止竞争条件和其他问题,将有助于测试,并使中间件更可重用。 此外,所有外部依赖都应该在初始化器中注入,这样在测试期间,您可以将其与模拟对象替换。如果您的中间件只使用到一个非常复杂对象的单个调用,而不是使用一个有许多函数要求的协议,请考虑创建一个只有一个函数要求的基本协议,或者甚至注入一个闭包,例如 @escaping (URLRequest) -> AnyPublisher<(Data, URLResponse), Error>。创建此类模拟将容易得多。 最后,考虑使用MiddlewareReader在依赖注入容器中包装这个中间件。 中间件示例 实现您的中间件时,您只需要处理传入的操作 public final class LoggerMiddleware: MiddlewareProtocol { public typealias InputActionType = AppGlobalAction // It wants to receive all possible app actions public typealias OutputActionType = Never // No action is generated from this Middleware public typealias StateType = AppGlobalState // It wants to read the whole app state private let logger: Logger private let now: () -> Date // Dependency Injection public init(logger: Logger = Logger.default, now: @escaping () -> Date) { self.logger = logger self.now = now } // inputAction: AppGlobalAction state: AppGlobalState output action: Never public func handle(action: InputActionType, from dispatcher: ActionSource, state: @escaping GetState<StateType>) -> IO<OutputActionType> { let stateBefore: AppGlobalState = state() let dateBefore = now() return IO { [weak self] output in guard let self = self else { return } let stateAfter = state() let dateAfter = self.now() let source = "\(dispatcher.file):\(dispatcher.line) - \(dispatcher.function) | \(dispatcher.info ?? "")" self.logger.log(action: action, from: source, before: stateBefore, after: stateAfter, dateBefore: dateBefore, dateAfter: dateAfter) } } } public final class FavoritesAPIMiddleware: MiddlewareProtocol { public typealias InputActionType = FavoritesAction // It wants to receive only actions related to Favorites public typealias OutputActionType = FavoritesAction // It wants to also dispatch actions related to Favorites public typealias StateType = FavoritesModel // It wants to read the app state that manages favorites private let api: API // Dependency Injection public init(api: API) { self.api = api } // inputAction: FavoritesAction state: FavoritesModel output action: FavoritesAction public func handle(action: InputActionType, from dispatcher: ActionSource, state: @escaping GetState<StateType>) -> IO<OutputActionType> { guard case let .toggleFavorite(movieId) = action else { return .pure() } let favoritesList = state() // state before reducer let makeFavorite = !favoritesList.contains(where: { $0.id == movieId }) return IO { [weak self] output in guard let self = self else { return } self.api.changeFavorite(id: movieId, makeFavorite: makeFavorite) (completion: { result in switch result { case let .success(value): output.dispatch(.changedFavorite(movieId, isFavorite: makeFavorite), info: "API.changeFavorite callback") case let .failure(error): output.dispatch(.changedFavoriteHasFailed(movieId, isFavorite: !makeFavorite, error: error), info: "api.changeFavorite callback") } }) } } } EffectMiddleware 这是一个旨在保持简单同时非常强大的中间件实现。对于每个传入的操作,您必须返回一个Effect,它只是根据您最喜欢的响应式库,对响应式可观察对象、发布者或SignalProducer进行包装。唯一条件是您的响应式流的输出(元素)必须是DispatchedAction,而错误必须是Never。DispatchedAction是一个具有操作自身和调度程序(操作源)的结构体,因此它对操作是通用的,并匹配EffectMiddleware的OutputAction。错误必须是Never,因为中间件预期解决所有副作用,包括错误。所以,如果您想处理错误,您可以在中间件中处理它;如果您想向用户警告错误,请在您的响应式流中捕获错误,将其转换为一个操作,如.somethingWentWrong(messageToTheUser: String)进行分发 并稍后将其缩减为AppState。 可选的EffectMiddleware还可以处理依赖项。这有助于将依赖注入中间件。如果依赖的泛型参数是Void,则中间件可以立即创建,无需传入任何依赖项,但是处理操作时不能使用任何外部依赖项。如果依赖的泛型参数具有一些类型或元组,则可以在处理操作时使用它们,但为了创建effect中间件,您需要提供该类型或元组。 重要:依赖项只能在Effect闭包内部可用,因为预期您“访问”外部世界仅在执行Effect时。 static let favouritesMiddleware = EffectMiddleware<FavoritesAction /* input action */, FavoritesAction /* output action */, AppState, FavouritesAPI /* dependencies */>.onAction { incomingAction, dispatcher, getState in switch incomingAction { case let .toggleFavorite(movieId): return Effect(token: "Any Hashable. Use this to cancel tasks, or to avoid two tasks of the same type") { context -> AnyPublisher<DispatchedAction<FavoritesAction>, Never> in let favoritesList = getState() let makeFavorite = !favoritesList.contains(where: { $0.id == movieId }) let api = context.dependencies return api.changeFavoritePublisher(id: movieId, makeFavorite: makeFavorite) .catch { error in DispatchedAction(.somethingWentWrong("Got an error: \(error)") } .eraseToAnyPublisher() } default: return .doNothing // Special type of Effect that, well, does nothing. } } Effect有一些有用的构造函数,如.doNothing、.fireAndForget、.just、.sequence、.promise、.toCancel等。此外,您可以将任何发布者、可观察对象或SignalProducer提升为Effect,只要它匹配所需的泛型参数,您可以使用.asEffect()函数。 Reducer Reducer是一个包裹在单例容器中的纯函数,它接受一个操作和当前状态来计算新状态。 MiddlewareProtocol管道可以执行两项操作:分发传出动作和处理传入动作。但他们不能做的是更改应用程序状态。中间件只能读取我们的应用的状态,但是当我们需要变异时,我们使用MutableReduceFunction函数。 (ActionType, inout StateType) -> Void 它与旧的ReduceFunction具有相同的语义(但性能更好) (ActionType, StateType) -> StateType 给定一个动作和当前状态(作为一个可变的输入输出),它计算新的状态并更改它 initial state is 42 action: increment reducer: increment 42 => new state 43 current state is 43 action: decrement reducer: decrement 43 => new state 42 current state is 42 action: half reducer: half 42 => new state 21 该函数正在将缓存的广告中的所有动作进行归约,并且它对于每个新的进入的动作进行增量归约。 理解这一点很重要,即reducer是一个同步操作,它在没有任何副作用(包括非显而易见的副作用,如创建Date()、使用DispatchQueue或Locale.current)的情况下计算新状态,所以永远不要向Reducer结构体中添加属性或调用任何外部函数。如果您倾向于这样做,请创建一个中间件,并通过它使用日期或地区分发动作。 还原器还负责保持状态的一致性,所以在更改状态之前始终进行最终的健康检查是个好主意,例如检查必须同时更改的其他依赖属性。 一旦reducer函数执行,存储将使用新的计算状态更新其单源,并将其传播到所有订阅者,他们将根据新状态反应并更新视图等。 此函数被包装在一个结构体中,以克服一些Swift限制,例如,允许我们将多个还原器组合成一个(单子操作,其中两个或多个还原器变成一个),或将还原器从局部类型提升到全局类型。 提升还原器的功能允许我们编写细粒度的“子还原器”,这些还原器将仅处理状态和/或动作的一部分,将其放置在不同框架和模块中,然后可以通过提供在全局和局部之间映射状态和动作的方法,将其插入更大的状态和动作处理程序中。有关更多信息,请参阅Lifting。 还原器的一个可能的实现是 let volumeReducer = Reducer<VolumeAction, VolumeState>.reduce { action, currentState in switch action { case .louder: currentState = VolumeState( isMute: false, // When increasing the volume, always unmute it. volume: min(100, currentState.volume + 5) ) case .quieter: currentState = VolumeState( isMute: currentState.isMute, volume: max(0, currentState.volume - 5) ) case .toggleMute: currentState = VolumeState( isMute: !currentState.isMute, volume: currentState.volume ) } } 请注意以下示例中的以下良好做法 没有DispatchQueue、线程、操作队列、承诺或响应式代码。 您需要实现此函数的所有内容都提供了action和currentState参数,不要使用来自全球范围的任何其他变量,甚至不用于读取。如果您需要其他东西,它应该要么在状态中,要么在动作有效载荷中。 不要启动副作用、请求、I/O、数据库调用。 在编写switch/case语句时避免使用default。这样,编译器可以帮助您更多。 尽可能使动作和状态泛型参数专一化。如果大状态是更大状态的一部分,不要诱使您将整个大状态传递给这个还原器。让它简短、简洁并具有针对性,这也帮助防止出现default情况或必须重新分配此还原器不会修改的属性。 ┌────────┐ IO closure ┌─▶│ View 1 │ ┌─────┐ (don't run yet) ┌─────┐ │ └────────┘ │ │ handle ┌──────────┐ ┌───────────────────────────────────────▶│ │ send │ ┌────────┐ │ ├────────▶│Middleware│──┘ │ │────────────▶├─▶│ View 2 │ │ │ Action │ Pipeline │──┐ ┌─────┐ reduce ┌──────────┐ │ │ New state │ └────────┘ │ │ └──────────┘ └─▶│ │───────▶│ Reducer │──────────▶│ │ │ ┌────────┐ ┌──────┐ dispatch │ │ │Store│ Action │ Pipeline │ New state │ │ └─▶│ View 3 │ │Button│─────────▶│Store│ │ │ + └──────────┘ │Store│ └────────┘ └──────┘ Action │ │ └─────┘ State │ │ dispatch ┌─────┐ │ │ │ │ ┌─────────────────────────┐ New Action │ │ │ │ │ │─run──▶│ IO closure ├────────────▶│Store│─ ─ ▶ ... │ │ │ │ │ │ │ │ │ │ │ │ └─┬───────────────────────┘ └─────┘ └─────┘ └─────┘ │ ▲ request│ side-effects │side-effects ▼ response ┌ ─ ─ ─ ─ ─ │ External │─ ─ async ─ ─ ─ │ World ─ ─ ─ ─ ─ ┘ 投影和提升 商店投射 提升 提升缩减器 使用闭包提升缩减器 使用KeyPath提升缩减器 提升中间件 使用闭包提升中间件 使用KeyPath提升中间件 可选转换 箭头的方向 使用KeyPaths 身份、忽略和荒谬 Xcode代码片段 存储投影 应用程序应该只有一个真实的“存储”,其中包含唯一的事实来源。然而,我们可以“派生”这个存储到称为存储分片的小子集中,这些小子集将处理状态或动作树的一部分,或者甚至完全不同的动作和状态,只要我们能将它们映射回原始存储类型。它不会存储任何内容,只投影原始存储。例如,视图可以定义完全定制的视图状态和视图动作,我们可以创建一个针对这些类型的StoreProjection,只要它支持真实的存储,并且类型可以以某种方式映射到视图状态和视图动作类型。存储分片将负责转换这些实体。 通常,您不希望视图能够访问整个应用程序状态或调度任何可能的全球应用程序操作。这不仅可能导致UI比所需的更频繁地刷新,还可能导致错误变得更多,将更复杂的代码放入视图层,并最终降低模块化,使视图与全局模型耦合在一起。 然而,您不希望将状态分割成多个部分,因为将它们保留在中心且唯一的点上可以确保一致性。同样,您不希望有多个地方分别处理动作,因为这可能会潜在的创建竞争条件。真实商店是全球状态唯一所有者,并有效处理动作的地方,这就是其应有的样子。 为了解决这两个问题,我们提供了一个按协议(StoreProjection)执行的操作,它遵守 StoreType 协议,因此从所有角度看都像一个真实的商店,但实际上它仅通过自定义类型使用状态和动作来投影真实商店,这是指你的模型的一部分(例如状态树中的分支),或者一个完全不同的实体,例如视图状态。一个 StoreProjection 有两个闭包,允许它在全局动作和视图使用的动作之间转换状态。这样,视图就不与全局模型耦合,而只是与其中的一小部分耦合,StoreProjection 中的闭包将负责提取/映射对视图有趣的部分。这样做也可以提高性能,因为视图只对全局状态中的相关属性进行刷新,而不是对任何属性。另一方面,视图可以调度有限的一组动作,这些动作将由 StoreProjection 中的闭包映射为全局动作。 可以从任何其他 StoreType 创建商店投影,甚至可以从另一个 StoreProjection 创建。这就像调用 StoreType/projection(action:state:) 并提供动作和状态映射闭包一样简单。 let storeProjection = store.projection( action: { viewAction in viewAction.toAppAction() } , state: { globalState in MyViewState.from(globalState: globalState) } ).asObservableViewModel(initialState: .empty) 有关真实存储和存储分片的更多信息,以及完整的代码示例,请参阅StoreType的文档。 提升 应用程序可能是一个复杂的产品,执行多个活动,而这些活动不一定相关。例如,同一个应用程序可能需要向天气API发出请求,使用CLLocation检查当前用户位置,并从NSUserDefaults中读取偏好设置。 尽管这些活动组合在一起以创建完整的体验,但可以将它们彼此隔离,以避免使用相同的资源,并导致竞争条件。同时,单独测试这些部分通常更容易,并导致更重要的测试。 理想情况下,我们应该组织我们的AppState和AppAction来考虑这些部分作为隔离的树。在上面的例子中,我们可以在AppState中有3个不同的属性,以及在我们的AppAction中有3个不同的枚举案例来分组与天气API、用户位置和NSUserDefaults访问相关的状态和动作。 如果我们根据模型中分组的3个路径将应用程序分为3种类型的Reducer和3种类型的MiddlewareProtocol,这将更有帮助。这些会非常有用。第一对Reducer和MiddlewareProtocol将是针对WeatherState和WeatherAction的通用的,第二对针对LocationState和LocationAction,第三对针对RepositoryState和RepositoryAction。它们甚至可以位于不同的框架中,这样编译器将阻止我们耦合Weather API代码与CLLocation代码,这是非常好的,因为这强制实行更好的做法并解锁代码重用。也许我们的CLLocation中继器/Reducer可以在完全不同的应用程序中检查公共交通路线时有用。 但最终,我们想将这些3种不同类型的实体放在一起,而我们的应用程序的StoreType“说话”的是AppAction和AppState,而不是由专门的处理器使用的子集。 enum AppAction { case weather(WeatherAction) case location(LocationAction) case repository(RepositoryAction) } struct AppState { let weather: WeatherState let location: LocationState let repository: RepositoryState } 对于针对WeatherAction和WeatherState的通用还原器,我们通过告诉还原器如何在全局树中找到所需属性,可以将它“提升”到全局类型AppAction和AppState。这将是\AppAction.weather和\AppState.weather。同样也可以为中间件,以及我们的应用程序中的其他2个还原器和中间件执行此操作。 当所有这些被提升到通用类型时,可以使用菱形运算符(<>)将它们组合在一起,并将其设置为存储处理程序。 重要:由于 Swift 中的枚举没有与结构体相同的 KeyPath,我们强烈建议阅读Action Enum Properties 文档,并为每个情况实现属性,无论是手动还是使用代码生成器,这样您就可以避免编写大量的易出错的 switch/case 代码。我们还提供了一些模板来帮助您完成这项工作。 让我们来探讨如何提升reducer和中间件。 提升Reducer Reducer有AppAction 输入、AppState 输入和AppState 输出,因为它只能处理动作(永远不会分散它们),读取状态和写入状态。 因此,提升方向应该是 Reducer: - ReducerAction? ← AppAction - ReducerState ←→ AppState 给出 // type 1 type 2 Reducer<ReducerAction, ReducerState> 转换 ╔═══════════════════╗ ║ ║ ╔═══════════════╗ ║ ║ ║ Reducer ║ .lift ║ Store ║ ╚═══════════════╝ ║ ║ │ ║ ║ ╚═══════════════════╝ │ │ │ │ ┌───────────┐ ┌─────┴─────┐ (AppAction) -> ReducerAction? │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ Reducer │ { $0.case?.reducerAction } │ │ Input Action │ Action │◀──────────────────────────────────────────────│ AppAction │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ KeyPath<AppAction, ReducerAction?> │ │ └─────┬─────┘ \AppAction.case?.reducerAction │ │ └───────────┘ │ │ │ get: (AppState) -> ReducerState │ { $0.reducerState } ┌───────────┐ ┌─────┴─────┐ set: (inout AppState, ReducerState) -> Void │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ Reducer │ { $0.reducerState = $1 } │ │ State │ State │◀─────────────────────────────────────────────▶│ AppState │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ WritableKeyPath<AppState, ReducerState> │ │ └─────┬─────┘ \AppState.reducerState │ │ └───────────┘ │ │ 使用闭包提升Reducer .lift( actionGetter: { (action: AppAction) -> ReducerAction? /* type 1 */ in // prism3 has associated value of ReducerAction, // and whole thing is Optional because Prism is always optional action.prism1?.prism2?.prism3 }, stateGetter: { (state: AppState) -> ReducerState /* type 2 */ in // property2: ReducerState state.property1.property2 }, stateSetter: { (state: inout AppState, newValue: ReducerState /* type 2 */) -> Void in // property2: ReducerState state.property1.property2 = newValue } ) 步骤 从头开始将Reducer的2个类型插入到3个闭包头中。 对于类型1,找到一个棱镜,从AppAction解析到匹配的类型。请确保已运行SOURCERY,并且所有枚举情况都由棱镜覆盖 对于状态获取闭包的类型2,找到从AppState解析到匹配类型的透镜(属性获取器)。 对于状态设置闭包的类型2,找到可以更改全局状态接收为新值(newValue)的透镜(属性设置器)。确保一切都可写。 使用KeyPath提升Reducer .lift( action: \AppAction.prism1?.prism2?.prism3, state: \AppState.property1.property2 ) 步骤 从上面的闭包示例开始 对于动作,我们可以使用来自\AppAction的KeyPath遍历棱镜树 对于状态,我们可以使用来自\AppState的可写KeyPath遍历属性,只要所有这些都声明为var,而不是let。 提升中间件 MiddlewareProtocol有AppAction 输入、AppAction 输出和AppState 输入,因为它可以处理动作,分散动作,并且只读状态( never 写它)。 因此,提升方向应该是 Middleware: - MiddlewareInputAction? ← AppAction - MiddlewareOutputAction → AppAction - MiddlewareState ← AppState 给出 // type 1 type 2 type 3 MyMiddleware<MiddlewareInputAction, MiddlewareOutputAction, MiddlewareState> 转换 ╔═══════════════════╗ ║ ║ ╔═══════════════╗ ║ ║ ║ Middleware ║ .lift ║ Store ║ ╚═══════════════╝ ║ ║ │ ║ ║ ╚═══════════════════╝ │ │ │ │ ┌───────────┐ ┌─────┴─────┐ (AppAction) -> MiddlewareInputAction? │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │Middleware │ { $0.case?.middlewareInputAction } │ │ Input Action │ Input │◀──────────────────────────────────────────────│ AppAction │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ Action │ KeyPath<AppAction, MiddlewareInputAction?> │ │ └─────┬─────┘ \AppAction.case?.middlewareInputAction │ │ └───────────┘ │ ┌─────┴─────┐ ┌───────────┐ (MiddlewareOutputAction) -> AppAction │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │Middleware │ { AppAction.case($0) } │ │ Output Action │ Output │──────────────────────────────────────────────▶│ AppAction │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ Action │ AppAction.case │ │ └───────────┘ │ │ │ └─────┬─────┘ ┌───────────┐ ┌─────┴─────┐ (AppState) -> MiddlewareState │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │Middleware │ { $0.middlewareState } │ │ State │ State │◀──────────────────────────────────────────────│ AppState │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ KeyPath<AppState, MiddlewareState> │ │ └─────┬─────┘ \AppState.middlewareState │ │ └───────────┘ │ │ 使用闭包提升中间件 .lift( inputAction: { (action: AppAction) -> MiddlewareInputAction? /* type 1 */ in // prism3 has associated value of MiddlewareInputAction, // and whole thing is Optional because Prism is always optional action.prism1?.prism2?.prism3 }, outputAction: { (local: MiddlewareOutputAction /* type 2 */) -> AppAction in // local is MiddlewareOutputAction, // an associated value for .prism3 AppAction.prism1(.prism2(.prism3(local))) }, state: { (state: AppState) -> MiddlewareState /* type 3 */ in // property2: MiddlewareState state.property1.property2 } ) 步骤 开始将 MyMiddleware 中的 3 种类型插入闭包头部。 对于类型1,找到一个棱镜,从AppAction解析到匹配的类型。请确保已运行SOURCERY,并且所有枚举情况都由棱镜覆盖 对于类型 2,从内部向外包装,直到达到 AppAction,在这个例子中,我们将它(即“它” = 本地)包装在 .prism3 中,然后包装在 .prism2 中,最后是 .prism1,最终到达 AppAction。 对于类型 3,找到从 AppState 解析到匹配类型的镜头(属性获取器)。 使用 KeyPath 提升中间件 .lift( inputAction: \AppAction.prism1?.prism2?.prism3, outputAction: Prism2.prism3, state: \AppState.property1.property2 ) .lift(outputAction: Prism1.prism2) .lift(outputAction: AppAction.prism1) 步骤 从上面的闭包示例开始 对于 inputAction,我们可以使用从 \AppAction 通过棱镜树遍历的 KeyPath 对于 outputAction 它不是一个 KeyPath,而是一个包装。因为我们不能同时包装超过一个层级,所以我们 为此使用闭包版本 逐级提升,从内部到外部,在这种情况下,遵循将本地包装到 Prism2(情况 .prism3)的步骤,然后将结果包装到 Prism1(情况 .prism2),最后将结果包装到 AppAction(情况 .prism1) 当只有一个层级时,没有必要担心 对于状态,我们可以使用从 \AppState 遍历属性的 KeyPath。 可选转换 如果某些动作正在通过存储器执行,一些减少器和中间件可能选择忽略它。例如,如果动作树与该中间件或减少器没有任何关联。这就是为什么,每个传入的动作(中间件的 InputAction 和还原器的简单动作)都是从 AppAction → Optional<Subset> 的转换。返回 nil 表示将忽略该动作。 对于其他方向,当中间件分发动作时,动作必须变为 AppAction,我们不能忽略中间件的话。 箭头方向 减少器 接收动作(输入动作)并且能够读写状态。 中间件 接收动作(输入动作),分发动作(输出动作)并且只读取状态(输入状态)。 在提升操作时,我们必须要记住这一点,因为它定义了变换的协变(covariant)/逆协变(contravariant),也就是所谓的映射或逆映射。 有一个特殊情况是对于reducer的State,因为那需要读写访问,换句话说,你被赋予了一个inout Whole和一个Part的新值,你使用这个新值来设置inout Whole中的正确路径。这正是WritableKeyPaths想要达成的目的,现在我们会更详细地说明。 KeyPaths的使用 KeyPath和Global -> Part转换是相同的,你给出以下方式的树描述:\Global.parent.part。 WritableKeyPath的用法语法类似,但更强大,允许我们转换(Global, Part) -> Global,或者(inout Global, Part) -> Void,两者是相同的。 但我们需要理解,只有当箭头的方向来自AppElement -> ReducerOrMiddlewareElement时,KeyPaths才是可能的,也就是说 Reducer: - ReducerAction? ← AppAction // Keypath is possible - ReducerState ←→ AppState // WritableKeyPath is possible Middleware: - MiddlewareInputAction? ← AppAction // KeyPath is possible - MiddlewareOutputAction → AppAction // NOT POSSIBLE - MiddlewareState ← AppState // KeyPath is possible 对于ReducerAction? ← AppAction和MiddlewareInputAction? ← AppAction,我们可以使用解析为Optional<ReducerOrMiddlewareAction>的KeyPaths { (globalAction: AppAction) -> ReducerOrMiddlewareAction? in globalAction.parent?.reducerOrMiddlewareAction } // or // KeyPath<AppAction, ReducerOrMiddlewareAction?> \AppAction.parent?.reducerOrMiddlewareAction 对于ReducerState ←→ AppState和MiddlewareState ← AppState转换,我们可以使用类似的语法,尽管reducer是inout(WritableKeyPath)。这意味着我们的整个树必须由var属性组成,而不是let。在这种情况下,除非Middleware或reducer接受Optional,否则转换不应该使用Optional。 { (globalState: AppState) -> PartState in globalState.something.thatsThePieceWeWant } { (globalState: inout AppState, newValue: PartState) -> Void in globalState.something.thatsThePieceWeWant = newValue } // or // KeyPath<AppState, PartState> or WritableKeyPath<AppState, PartState> \AppState.something.thatsThePieceWeWant // where: // var something // var thatsThePieceWeWant 对于MiddlewareOutputAction → AppAction,我们不能使用keypath,因为它没有意义,因为方向与我们的目标相反。在这种情况下,我们不是从全局值中展开/提取部分,而是从某个Middleware提供了特定操作,我们需要将其包装到AppAction中。这可以通过两种形式来实现 { (middlewareAction: MiddlewareAction) -> AppAction in AppAction.treeForMiddlewareAction(middlewareAction) } // or simply AppAction.treeForMiddlewareAction // please notice, not KeyPath, it doesn't start by \ 然而,短形式不能一次穿越两层 { (middlewareAction: MiddlewareAction) -> AppAction in AppAction.firstLevel( FirstLevel.secondLevel(middlewareAction) ) } // this will NOT compile (although a better Prism could solve that, probably): AppAction.firstLevel.secondLevel // You could try, however, to lift twice: .lift(outputAction: FirstLevel.secondLevel) // Notice that first we wrap the middleware value in the second level .lift(outputAction: AppAction.firstLevel) // And then we wrap the first level in the AppAction // The order must be from inside to outside, always. 同一性、忽略和荒谬 Void 当Middleware不需要State时,它可以是无(Void) 使用ignore提升Void,它相当于{ (_: Anything) -> Void in } Never 当Middleware不需要分发操作时,它可以是Never 使用absurd提升Never,它相当于{ (never: Never) -> Anything in } Identity 当某些部分的提升应该保持不变,因为它们已经处于期望的类型时 使用identity提升这些,它相当于{ $0 } 背后的理论:Void和Never是互为对偶 任何东西都可以成为Void(终端对象) Never(初始对象)可以成为任何东西 Void有1个可能的实例(它是单例) Never没有可能的实例 因为没有人能给你Never,你可以把Anything作为一种挑战。这就是为什么函数被称为ab(绝无可能)的原因,你不能调用它。 >Xcode Snippets // Reducer expanded .lift( actionGetter: { (action: AppAction) -> <#LocalAction#>? in action.<#something?.child#> }, stateGetter: { (state: AppState) -> <#LocalState#> in state.<#something.child#> }, stateSetter: { (state: inout AppState, newValue: <#LocalState#>) -> Void in state.<#something.child#> = newValue } ) // Reducer KeyPath: .lift( action: \AppAction.<#something?.child#>, state: \AppState.<#something.child#> ) // Middleware expanded .lift( inputAction: { (action: AppAction) -> <#LocalAction#>? in action.<#something?.child#> }, outputAction: { (local: <#LocalAction#>) -> AppAction in AppAction.<#something(.child(local))#> }, state: { (state: AppState) -> <#LocalState#> in state.<#something.child#> } ) // Middleware KeyPath .lift( inputAction: \AppAction.<#local#>, outputAction: AppAction.<#local#>, // not more than 1 level state: \AppState.<#local#> ) 架构 这种数据流在某种程度上是MVC的实现,与苹果的MVC有很大不同,它提供了一个非常严格和声明式的层责任描述,并通过更深入地定义其实施方式来强制Model层的增长:在这种情况下,Model是Store。所有Controller需要做的就是将视图操作转发给Store,并订阅状态变化,在必要时更新视图。如果这个流程听起来不像MVC,让我们看一下苹果网站上的截图 一个重要的区别是关于用户操作:在SwiftRex中,它由控制器转发并到达Store,因此更新状态的责任现在成为Store的责任。其余部分几乎一样,但是对Model的操作有更好的定义。 ╼━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╾ ╱░░░░░░░░░░░░░░░░░◉░░░░░░░░░░░░░░░░░░╲ ╱░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░╲ ┃░░░░░░░░░░░░░◉░░◖■■■■■■■◗░░░░░░░░░░░░░░░░░┃ ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃ ╭┃░╭━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╮░┃ │┃░┃ ┌──────────┐ ┃░┃ ╰┃░┃ │ UIButton │────────┐ ┃░┃ ┃░┃ └──────────┘ │ ┃░┃ ╭┃░┃ ┌───────────────────┐ │ ┃░┃╮ dispatch<Action>(_ action: Action) │┃░┃ │UIGestureRecognizer│───┼──────────────────────────────────────────────┐ │┃░┃ └───────────────────┘ │ ┃░┃│ │ ╰┃░┃ ┌───────────┐ │ ┃░┃│ ▼ ╭┃░┃ │viewDidLoad│───────┘ ┃░┃╯ ┏━━━━━━━━━━━━━━━━━━━━┓ │┃░┃ └───────────┘ ┃░┃ ┃ ┃░ │┃░┃ ┃░┃ ┃ ┃░ ╰┃░┃ ┃░┃ ┃ ┃░ ┃░┃ ┌───────┐ ┃░┃ ┃ ┃░ ┃░┃ │UILabel│◀─ ─ ─ ─ ┐ ┃░┃ ┃ ┃░ ┃░┃ └───────┘ ┃░┃ Combine, RxSwift ┌ ─ ─ ┻ ─ ┐ ┃░ ┃░┃ │ ┃░┃ or ReactiveSwift State Store ┃░ ┃░┃ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╋░─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│Publisher│ ┃░ ┃░┃ ▼ │ ┃░┃ subscribe(onNext:) ┃░ ┃░┃ ┌─────────────┐ ▼ ┃░┃ sink(receiveValue:) └ ─ ─ ┳ ─ ┘ ┃░ ┃░┃ │ Diffable │ ┌─────────────┐ ┃░┃ assign(to:on:) ┃ ┃░ ┃░┃ │ DataSource │ │RxDataSources│ ┃░┃ ┃ ┃░ ┃░┃ └─────────────┘ └─────────────┘ ┃░┃ ┃ ┃░ ┃░┃ │ │ ┃░┃ ┃ ┃░ ┃░┃ ┌──────▼───────────────▼───────────┐ ┃░┃ ┗━━━━━━━━━━━━━━━━━━━━┛░ ┃░┃ │ │ ┃░┃ ░░░░░░░░░░░░░░░░░░░░░░ ┃░┃ │ │ ┃░┃ ┃░┃ │ │ ┃░┃ ┃░┃ │ │ ┃░┃ ┃░┃ │ UICollectionView │ ┃░┃ ┃░┃ │ │ ┃░┃ ┃░┃ │ │ ┃░┃ ┃░┃ │ │ ┃░┃ ┃░┃ │ │ ┃░┃ ┃░┃ └──────────────────────────────────┘ ┃░┃ ┃░╰━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╯░┃ ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃ ┃░░░░░░░░░░░░░░░░░░░▓▓▓▓░░░░░░░░░░░░░░░░░░░┃ ┃░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░┃ ╲░░░░░░░░░░░░░░░░░░▓▓▓▓░░░░░░░░░░░░░░░░░░╱ ╲░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░╱ ╼━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╾ 您可以认为Store是一个非常重的“Model”层,完全独立于视图和控制器,其中所有业务逻辑都存在。乍一看,这可能看起来像是将“大量”问题从一个层面转移到了另一个层面,这就是为什么Store什么都不是,只是一种集合可组合盒子,具有非常明确的角色和,最重要的是,限制。 ╼━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╾ ╱░░░░░░░░░░░░░░░░░◉░░░░░░░░░░░░░░░░░░╲ ╱░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░╲ ┃░░░░░░░░░░░░░◉░░◖■■■■■■■◗░░░░░░░░░░░░░░░░░┃ ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃ ╭┃░╭━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╮░┃ │┃░┃ ┌────────┐ ┃░┃ ╰┃░┃ │ Button │────────┐ ┃░┃ ┃░┃ └────────┘ │ ┃░┃ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┏━━━━━━━━━━━━━━━━━━━━━━━┓ ╭┃░┃ ┌──────────────────┐ │ ┃░┃╮ dispatch ┃ ┃░ │┃░┃ │ Toggle │───┼────────────────────▶│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ │────────────▶┃ ┃░ │┃░┃ └──────────────────┘ │ ┃░┃│ view event f: (Event) → Action app action ┃ ┃░ ╰┃░┃ ┌──────────┐ │ ┃░┃│ │ │ ┃ ┃░ ╭┃░┃ │ onAppear │───────┘ ┃░┃╯ ┃ ┃░ │┃░┃ └──────────┘ ┃░┃ │ ObservableViewModel │ ┃ ┃░ │┃░┃ ┃░┃ ┃ ┃░ ╰┃░┃ ┃░┃ │ a projection of │ projection ┃ Store ┃░ ┃░┃ ┃░┃ the actual store ┃ ┃░ ┃░┃ ┃░┃ │ │ ┃ ┃░ ┃░┃ ┌────────────────────────┐ ┃░┃ ┃ ┃░ ┃░┃ │ │ ┃░┃ │ │ ┌┃─ ─ ─ ─ ─ ┐ ┃░ ┃░┃ │ @ObservedObject │◀ ─ ─ ╋░─ ─ ─ ─ ─ ─ ─ ─ ◀─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ◀─ ─ ─ ─ ─ ─ State ┃░ ┃░┃ │ │ ┃░┃ view state │ f: (State) → View │ app state │ Publisher │ ┃░ ┃░┃ └────────────────────────┘ ┃░┃ State ┳ ─ ─ ─ ─ ─ ┃░ ┃░┃ │ │ │ ┃░┃ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ┗━━━━━━━━━━━━━━━━━━━━━━━┛░ ┃░┃ ▼ ▼ ▼ ┃░┃ ░░░░░░░░░░░░░░░░░░░░░░░░░ ┃░┃ ┌────────┐ ┌────────┐ ┌────────┐ ┃░┃ ┃░┃ │ Text │ │ List │ │ForEach │ ┃░┃ ┃░┃ └────────┘ └────────┘ └────────┘ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░┃ ┃░╰━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╯░┃ ┃░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░┃ ┃░░░░░░░░░░░░░░░░░░░▓▓▓▓░░░░░░░░░░░░░░░░░░░┃ ┃░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░┃ ╲░░░░░░░░░░░░░░░░░░▓▓▓▓░░░░░░░░░░░░░░░░░░╱ ╲░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░╱ ╼━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╾ 那么SwiftUI怎么样?这种架构适合新的UI框架吗?事实上,在SwiftUI中,这种架构甚至工作得更好,因为SwiftUI受到几个函数式模式的影响,而且天生就是响应式和无状态的。在WWDC 2019期间提到多次,在SwiftUI中,“视图是状态的函数”,我们应始终追求单一的真理源,并且数据应始终单向流动。 安装 CocoaPods 在项目的根目录下创建或修改Podfile。您的设置将取决于您选择的ReactiveFramework。 对于Combine # Podfile source 'https://github.com/CocoaPods/Specs.git' use_frameworks! target 'MyAppTarget' do pod 'CombineRex' end 对于RxSwift # Podfile source 'https://github.com/CocoaPods/Specs.git' use_frameworks! target 'MyAppTarget' do pod 'RxSwiftRex' end 对于ReactiveSwift # Podfile source 'https://github.com/CocoaPods/Specs.git' use_frameworks! target 'MyAppTarget' do pod 'ReactiveSwiftRex' end 如上所述,有些行是可选的,因为最终的Podspecs已经包含了正确的依赖项。 然后,您只需安装Pod并打开.xcworkspace文件,而不是.xcodeproj文件。 $ pod install $ xed . Swift包管理器 在项目根目录中创建或修改Package.swift文件。您可以使用自动连接模式(静态/动态),或者使用后缀为Dynamic的项目,强制动态连接,从而克服目前Xcode在解决菱形依赖问题方面的局限性。 如果您只从一个目标使用它,自动模式应该就可以了。 组合,自动连接模式 // swift-tools-version:5.5 import PackageDescription let package = Package( name: "MyApp", platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], products: [ .executable(name: "MyApp", targets: ["MyApp"]) ], dependencies: [ .package(url: "https://github.com/SwiftRex/SwiftRex.git", from: "0.8.12") ], targets: [ .target(name: "MyApp", dependencies: [.product(name: "CombineRex", package: "SwiftRex")]) ] ) RxSwift,自动连接模式 // swift-tools-version:5.5 import PackageDescription let package = Package( name: "MyApp", platforms: [.macOS(.v10_10), .iOS(.v8), .tvOS(.v9), .watchOS(.v3)], products: [ .executable(name: "MyApp", targets: ["MyApp"]) ], dependencies: [ .package(url: "https://github.com/SwiftRex/SwiftRex.git", from: "0.8.12") ], targets: [ .target(name: "MyApp", dependencies: [.product(name: "RxSwiftRex", package: "SwiftRex")]) ] ) ReactiveSwift,自动连接模式 // swift-tools-version:5.5 import PackageDescription let package = Package( name: "MyApp", platforms: [.macOS(.v10_10), .iOS(.v8), .tvOS(.v9), .watchOS(.v3)], products: [ .executable(name: "MyApp", targets: ["MyApp"]) ], dependencies: [ .package(url: "https://github.com/SwiftRex/SwiftRex.git", from: "0.8.12") ], targets: [ .target(name: "MyApp", dependencies: [.product(name: "ReactiveSwiftRex", package: "SwiftRex")]) ] ) 组合,动态连接模式(类似地,对于RxSwift或ReactiveSwift产品,也使用添加"Dynamic"的方法) // swift-tools-version:5.5 import PackageDescription let package = Package( name: "MyApp", platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], products: [ .executable(name: "MyApp", targets: ["MyApp"]) ], dependencies: [ .package(url: "https://github.com/SwiftRex/SwiftRex.git", from: "0.8.12") ], targets: [ .target(name: "MyApp", dependencies: [.product(name: "CombineRexDynamic", package: "SwiftRex")]) ] ) 然后,您可以在终端上构建,或者使用现在原生支持SPM的Xcode 11或更高版本。 $ swift build $ xed . 重要:对于Xcode 12,请使用版本0.8.8。从版本0.9.0开始,需要Xcode 13。 Carthage 由于缺乏兴趣和高维护成本,Carthage不再受支持。 如果您认为这非常重要,请创建一个GitHub问题并告知我们,我们将评估恢复正常支持的可行性。在此期间,您可以检查最后一个兼容Carthage的版本,该版本为0.7.1,并最终将目标设置为该版本,直到我们提出更好的解决方案。