赶时间吗?已经熟悉其他 Redux 实现?
没问题,我们有一个 TL;DR 快速指南,以非常实用的方式展示了您关于 SwiftRex 需要知道的最少内容。
我们仍建议您阅读完整的 README,以便更好地理解 SwiftRex 的概念。
目标
如今,许多针对移动应用的架构和设计模式被提出来解决与单职责原则(如巨大量级的视图控制器)相关的问题,或提高可测试性和依赖项管理。对于移动开发者来说,其他一些常见挑战,例如状态处理、竞争条件、模块化/组件化、线程安全或正确处理UI生命周期和所有权,虽然探索较少,但也可能对应用造成同等危害。
管理所有这些问题可能听起来是一项看似不可能完成的任务,需要大量模式和复杂的测试场景。毕竟,如何复制只有在您的部分用户中出现但从未在开发人员设备中发生的罕见但关键错误?这可能会令人沮丧,而且我们中大多数人可能都曾不时遇到过这样的问题。
这正是SwiftRex大放异彩的场景,因为它
强制实施单职责原则 [点击展开]
一些架构非常灵活,允许我们在任何地方添加任何代码。这对于大多数由单一人员开发的小型应用来说是不错的,但一旦项目团队开始扩张,某些层将变得非常大,承担太多职责,有隐式的副作用、竞争条件和其他错误。在这种情况下,可测试性也受到了损害,应用的各个部分之间的一致性也受到损害,因此寻找和修复错误变得非常困难。
SwiftRex通过规定代码应该放置的位置以及该层限制的非常严格的策略来防止这种情况,这个策略通常由编译器强制执行。好吧,这听起来很困难且复杂,但实际上它比传统模式更容易,因为一旦你理解了这个架构,你就清楚地知道该怎么做,你清楚地知道基于其职责在哪找到某行代码,你清楚地知道如何测试每个组件,并且非常清楚地了解每个层的边界。
为每个层提供明确的测试策略(《也请查看TestingExtensions》点击展开)
我们相信,一个架构不仅必须非常可测试,而同时还必须提供测试其每个层明确的指南。如果一个层只有一个任务,并且这个任务可以通过根据给定的输入的所有时间基于预期输出的断言来验证,测试将更有意义和广泛,所以当创建新功能时不会引入回归。
SwiftRex架构的大多数层将是纯函数,这意味着所有计算都仅从输入参数中完成,所有结果都将暴露在输出中,没有隐式的副作用或全局作用域的访问。测试不需要模拟、存根、依赖注入或任何类型的准备,你只需用一个值调用一个函数,检查结果,就是这样。
这同样适用于UI层、表示层、reducers和状态发布者,因为这个整个链是将纯函数组合起来的。唯一需要依赖注入和模拟的层是中间件,因为它是唯一一个依赖于服务和向外界产生副作用的层。幸运的是,因为中间件是可组合的,我们可以将其分解为只有一项任务的小块,测试因此变得更愉快且容易,因为不需要模拟数百个组件,你只需要注入一个。
我们还提供了《TestingExtensions》,它允许我们使用一个将验证所有SwiftRex层的DSL语法来测试整个用例,确保没有发生意外的副作用或动作,并且状态是按预期逐步更改的。这是用少量和易于编写的行测试整个应用的一种强大而有趣的方式。
在不能更改状态的组合/可重用中间件盒子中隔离所有副作用 [点击展开]
如果一个层需要同时处理多个服务,并且以异步方式响应时改变状态,那么保持这种状态一致性并防止竞态条件是很困难的。由于一个效果可能会干扰另一个效果,因此也使得测试变得更加困难。
多年来,苹果和社区为访问网页或网络服务和设备中的传感器创建了惊人的框架。不幸的是,一些框架依赖于委托模式,一些使用闭包/回调,一些使用通知中心、KVO或响应流。组合这种通知形式的混合将需要布尔标志、计数器和其他隐式状态,最终由于竞态条件而破裂。
响应式框架有助于使这一切更统一和可组合,特别是一起使用它们的Cocoa扩展时。事实上,苹果也意识到了这一点,2019年WWDC中有很大一部分聚焦于使用新引入的框架Combine和SwiftUI来演示和解决这个问题。
但是在响应式管道中组合大量服务并不总是容易,而且有自己的陷阱,比如由于某个流发出错误而进行的完全管道取消、事件重入性,以及最后但并非最不重要的是,掌握多个操作符的陡峭学习曲线。
SwiftRex大量使用响应式编程,并允许您根据舒适度使用。然而,我们也提供了一种更统一的方式来组合不同的服务,只需1种数据类型和2个操作符:中间件,`<>`操作符和`lift`操作符,所有其他操作都可以通过触发操作到自身、其他中间件或状态还原器来简化。您仍然可以选择创建一个更大的中间件,像传统响应流一样处理多个源,如果您喜欢,但这可能对经验不足的开发人员来说过于庞大,更难测试,更难在不同应用程序中重用。
因为这个主题非常广泛,所以它将在中间件文档中解释得更好。
最小化了依赖ViewControllers/Presenters/Interactors/SwiftUI Views的使用[点击展开]
在浏览应用程序时传递依赖从未是一件容易的事情:ViewControllers初始化器非常棘手,您必须始终考虑该类是从NIB/XIB、编程方式还是从故事板中创建的,然后编写正确的init方法,不仅传递这个类需要的所有依赖性,还要传递其子视图控制器和将要推送按钮时将要显示的下一个视图控制器所需的依赖性,因此您必须在通过视图的路由过程中发送数十个依赖项。如果不使用初始化器,而是使用属性赋值,则必须使用隐式未包装的属性,这不是很好。
协调器/线框模式当然有助于此,但 somehow您将问题转移到了路由器,它们也需要保留更多实际使用之外但下一路由器将要使用的依赖项。您可以使用服务定位器模式,例如流行的环境方法,这实际上是一种处理问题的简单方法。然而,测试这个单例可能会有点棘手,因为,好吧,它是一个单例。还有一些人不喜欢隐式注入,而更喜欢显式添加一个层需要的功能性依赖。
所以,这可能看起来是不可能解决的问题,并让每个人都满意,对吗?嗯,其实不是。如果您的视图控制器只需要一个名为“Store”的单个依赖项,从这个依赖项获取所需的状态,并将所有用户事件分派到它而不是实际执行任何工作,那会怎么样?在这种情况下,无论您是使用显式注入还是服务定位器,注入存储都会变得更容易。
好的,但仍然有人需要做这项工作,这正是中间件执行的任务。在SwiftRex中,中间件应该在应用的入口点创建,即在配置和准备依赖项之后。然后创建所有中间件,注入它们完成工作所需的一切(希望每个中间件不超过2个依赖项,这样你知道它们没有承担太多责任)。最后,将它们组合在一起并开始你的存储。中间件可以有定时器或纯粹响应来自UI的动作,但它们是唯一具有副作用的一层,因此是唯一需要服务依赖的一层。
最后,你可以将区域、语言和界面特性添加到你的全局状态中,即使你需要在状态中创建数字和日期格式化器,也可以做到这一点,而无需依赖注入,甚至更好,当用户决定更改iOS设置时能够正确响应。
完全从UI生命周期及其所有权树中分离状态、服务、突变和其他副作用[点击展开]
UIViewControllers有一个非常独特的所有权模型:您无法控制它。视图控制器在导航堆栈中保持内存状态,或者在界面被展示时,或者在标签页呈现时,但它们可以在任何时候被释放,因此,所有归属在视图控制器下的事物都可以被释放。我们一直在使用并喜爱的那些[weak self]实际上有时候也可以是弱引用,而且在我们“else返回”时很容易不进行推理。任何无论屏幕显示与否都必须完成的重要任务,都不应该处于视图控制器生命周期中,因为用户可以很容易地关闭您的模态或弹出视图。SwiftUI已经改进了这一点,但仍有可能从视图的闭包中启动异步任务,尽管现在视图是值类型,做出这些错误有点困难,但仍然可能。
SwiftRex通过强制要求所有和每一个副作用或异步任务都由中间件而不是视图来完成,来解决此问题。中间件的生命周期由存储拥有,因此我们不应该期望在任何不幸的惊讶,只要存储在应用期间存在。
您仍然可以从视图中派发"viewDidLoad"、"onAppear"、"onDisappear"事件,以便执行任务取消,这样您就获得更多的控制,而不失控制。
有关更多信息请点击此链接
消除竞争条件[点击展开]
当一个应用程序必须处理来自不同服务和来源的信息时,通常需要一些小的布尔标志来检查某个任务是否完成或失败。这通常是因为某些服务通过代理、某些通过闭包以及几种其他创造性方式返回。使用标志同步这些多个来源,或者从并发任务中突变相同的变量或数组,可以导致真正奇怪的错误和崩溃,这通常是很难捕捉、理解和修复的。
处理锁和派发队列可以帮助实现这一点,但以苏联式的方式反复这样做既乏味又危险,必须编写考虑所有可能路径和时机的测试,而且其中一些测试最终可能会变得不可靠,因为在竞争条件下仍然存在。
通过让应用中所有事件都通过同一个队列进行处理,SwiftRex 最终将以一致的方式泛化全局状态,从而防止竞态条件的发生。首先,由于中间件和异步任务是唯一副作用和异步任务的来源,因此保持中间件小而专注是一个任务可以简化竞态条件的测试,特别是在您保持它们小并且专注于单个任务的情况下。在这种情况下,您的响应将按照先进先出的顺序排队,并由所有的还原器一次性处理。其次,由于还原器是状态变动的守门人,使其没有副作用对于成功和一致的状态变动至关重要。最后但同样重要的是,所有的事情都是针对操作进行的,并且操作可以轻松地在崩溃报告中或以日志记录进行记录,包括谁分发了该操作,因此即使您仍然发现发生了竞态条件,也可以轻松理解哪些操作正在更改状态以及这些操作来自哪里。
允许更安全的类型编码风格[点击展开]
Swift 泛型有点难学,还有与之相关的协议关联类型。SwiftRex 不要求您精通泛型,理解协变或类型擦除,但肯定会越深入,就越能够编写由编译器验证而不是单元测试验证的应用。将运行时错误从编译时移除是一个我们所有人都应接受的重要目标。与尝试解决 Swift 类型系统相比,在应用发布到世界各地之后检查崩溃报告可能更好。这正是 Swift 带来的静态类型语言的思维方式,一个就连可空性也是类型安全性的语言,多亏了 Optional,我们现在可以安心地知道,除非我们非安全地并且明确选择这样做,否则我们不会访问空指针。
SwiftRex 对在使用强类型事件/操作和状态进行约束:存储的行动分发器、中间件的行动处理器、中间件的行动输出、还原器的行动和状态输入输出,最后还有存储状态观察。整个流程都是强类型的,因此编译器可以防止错误或运行时错误。
此外,中间件、还原器和存储都可以从部分状态和操作转换为全局状态和操作。这意味着您可以编写一个在特定领域工作的强类型模块,例如网络可达性。您的中间件和还原器将“说”网络领域状态和操作,例如是否连接,是 WiFi 还是 LTE,是否更改了连接状态,等等。然后您可以通过提供两个映射函数将这些组件(中间件和还原器)提升到应用的全局状态:一个用于提升状态,另一个用于提升操作。多亏了泛型,这个操作完全是类型安全的。同样,可以从主存储中“导出”存储投影。存储投影实现了存储必须具有的两个方法(输入操作和输出状态),但它不是一个真正的存储,它只映射全局状态和操作到更局部的领域,这意味着,将视图事件转换为操作,将视图状态转换为领域状态。
有了这些工具,我们相信您可以编写一个从头到尾都是类型安全的 应用。
帮助实现模块化、组件化和项目间的代码重用[点击展开]
中间件应专注于一个非常非常小的域,仅执行一种类型的工作,并以操作的形式返回。归约器应专注于非常小的动作和状态的组合。视图应该只访问一小部分状态,或者理想情况下是使用直接映射到文本字段字符串、切换的布尔值、进度条的0.0到1.0等原语来表示应用全局状态的视图状态。
然后,你可以将这三个组件——中间件、归约器、存储投影——提升到应用实际需要的全局状态和动作。
SwiftRex允许我们创建小的工作单元,仅在需要时将其提升到全局域,因此Swift框架可以在非常具体的域中运行,并覆盖测试和Playgrounds/SwiftUI预览,无需启动整个应用。一旦这个框架准备就绪,我们只需将其插入我们的应用即可,甚至更好的是,多个应用。专注于小的域将解锁更好的抽象,当这个抽象从中间件(副作用)到视图时,你就拥有了一个定义构建块的有力工具。
强制执行单一真相源和适当的状态管理 [点击展开]
使用SwiftRex可以创建一个可信的单一真相源,这个真相源在屏幕之间永远不会不一致或不同步。所有状态都在一个地方,单个包含所有内容的树,这可能令人恐惧。当你将所有内容聚集在单个位置时,你可能会意识到你需要多少状态,这可能会令人恐惧。但不用担心,这不是你没有的东西,它已经在那里了,在ViewController中,在Presenter中,在用于控制服务结果的标志中,但由于它分布得很广,你没有意识到它是如此之大。更糟糕的是,这会导致重复,因为当你需要从两个不同的地方获取相同的信息时,更容易复制并希望它们能正确地保持同步。
事实上,当你将你的整个应用状态聚集在统一的树中时,你开始消除不再需要的许多事物,你最终的状态会比混乱的状态更小。
正确编写全局状态和全局动作树可能具有挑战性,但这可能是工程师必须做的最重要的任务。
有关更多信息 请点击此链接
提供开发、测试和调试工具 [点击展开]
一些项目提供SwiftRex工具来帮助开发者编写应用、测试、调试以及评估崩溃报告。
CombineRextensions provides SwiftUI 扩展以与 CombineRex 一起工作,TestingExtensions 提供了“测试断言”,可以以有趣且简单的方式解锁用例的可测试性,InstrumentationMiddleware 允许您使用 Instruments 查看 SwiftRex 应用中发生了什么,SwiftRexMonitor 将是一个 Redux DevTools 的 Swift 版本,您可以从外部的 Mac 或 iOS 设备远程监控应用的状态和动作,甚至注入动作来模拟副作用(例如用于 UITests),GatedMiddleware 是一个中间件包装器,可以在运行时启用或禁用其他中间件,LoggerMiddleware 是一个功能强大的日志记录器,可以帮助开发者轻松理解运行时发生了什么。更多中间件即将开源,例如创建出色的 Crashlytics 报告,让您以前从未拥有的方式来讲述崩溃的故事,从而重新创建崩溃或用户报告。还有生成代码的工具(如 Sourcery 模板、Xcode 段落和模板、控制台工具),还有更高的 API 级别,如 EffectMiddlewares,允许我们创建单个函数的中间件,就像 Reducers 一样简单,或者 Handler,允许将中间件和 Reducers 组织在同一结构下,以便一起提升。即将发布新的依赖注入策略。
所有这些工具已经完成,并将很快发布,未来还有更多。
我不会撒谎,这是一种完全不同的写应用的方式,就像大多数反应式方法一样;但一旦您习惯了,它就更有意义,并使您能够在项目之间重用更多代码,为您提供更好的软件开发、测试、调试、日志记录工具,并最终以前从未做过的方式考虑事件、状态和变化。我保证,这将是一条没有回头路的方式,一个单向的旅程。
反应式框架库
SwiftRex 目前支持 3 个主要反应式框架
稍后可以很容易地通过实现可以在 ReactiveWrappers.swift
文件中找到的一些抽象桥接来添加更多框架。为了防止向您的应用添加不必要的文件,SwiftRex 分为 4 个包
- SwiftRex:核心库
- CombineRex:Combine 框架的实装
- RxSwiftRex:RxSwift 框架的实装
- ReactiveSwiftRex:ReactiveSwift 框架的实装
SwiftRex 本身就不够了,您必须选择这三个实装中的一个。
部件
让我们通过分成 3 个部分来理解 SwiftRex 的组成部分
概念部分
行为
行为代表由外部(或有时为内部)参与者通知的事件。它与相关的输入事件有关。
SwiftRex中没有“行为”协议或类型。然而,行为将作为大多数核心数据结构的泛型参数出现,这意味着定义根行为类型的工作由您自己完成。
从概念上讲,我们可以这样说,行为代表着来自您的应用外部(或有时为内部)参与者发生的事情,这意味着用户交互、定时器回调、网络服务的响应、CoreLocation和其他框架的回调。但一些内部参与者也可以启动行为。例如,当UIKit完成加载您的视图时,我们可以将viewDidLoad
视为一种行为,如果我们对此事件感兴趣的话。同样的,SwiftUI视图(.onAppear
、.onDisappear
、.onTap
)或手势修饰符(.onEnded
、.onChanged
、.updating
)也可以被视为行为。当我们从URLSession收到可以解析为结构的Data时,这是一个成功的行动,但如果是404或JSONDecoder不能理解有效负载,这应该也成为失败行为。Notification Center所做的没有别的,就是通知系统各处的行为,例如键盘关闭或设备旋转。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中的枚举没有键路径,如同struct,我们强烈建议您阅读操作枚举属性文档,并为每个情况实现属性,无论是手动完成还是使用代码生成器,以避免以后编写大量易出错的switch/case。我们还提供了一些模板以帮助您。
状态
状态表示应用程序在开启期间所持有的一切知识,通常存储在内存中且可变。它是关于相关输出属性的。
SwiftRex中没有“状态”协议或类型。然而,状态将作为大多数核心数据结构的泛型参数被找到,这意味着定义根状态类型的责任在于您。
从概念上讲,我们可以说状态代表应用程序在开启期间所持有的一切知识,通常存储在内存中且可变;它就像一张纸,在那里记录一些值,对于您接收到的每个动作,您会消除一个值并替换为另一个值。另一种思考状态的方式是采用函数式编程的方式:状态不被持久化,但它是通过一个函数计算得来的,这个函数接受应用程序的初始状态以及自启动以来接收到的所有动作,并按时间顺序应用所有动作更改以计算当前值。这被称为事件存储设计模式,最近在 一些Web后端服务中变得流行,例如 Kafka事件存储。
在电池和内存有限设备的本例中,我们无法承担真正的生成模式,因为它会在每次请求简单布尔值时重建整个应用程序历史,这会花费太多。因此,每次收到动作时我们都会“缓存”新的状态,这正是我们在SwiftRex中所谓的“状态”。因此,也许我们可以混合这两种关于状态的思考方式,并针对状态的性质提出一个更好的概括。
状态是一个函数的结果,该函数接受两个参数:先前的(或初始的)状态以及发生的一些动作,以确定新状态。随着更多动作的到来,这将逐渐发生。状态对于向用户输出数据是有用的。
然而,要小心,有些东西可能像状态,但并不是。假设您有一个向用户展示商品价格的 application。这个价格将在美国显示为“$3.00”,在德国显示为“$3,00”,也许这个商品可以以英磅列出,因此在美国我们应该显示“£3.00”,而在德国则是“£3,00”。在这个例子中我们有:
- 货币类型(
£
或$
)
- 数值(
3
)
- 区域设置(
en_US
或de_DE
)
- 格式化字符串(
"$3.00"
、"$3,00"
、"£3.00"
或"£3,00"
)
格式化字符串本身不是状态,因为它可以从其他属性计算得出。这可以被称为“派生状态”,保留这种状态可能会导致不一致。我们每次其他任何一个属性改变时,都得记得更新这个值。因此,将这个String表示为计算属性或其他3个值的函数会更好。这种派生状态的最好位置是在视图或控制器中,除非重新计算的成本很高;在这种情况下,可以将它们存储在状态中,但要非常小心。幸运的是,SwiftRex能够帮助保持状态的一致性,就像我们在Reducer部分将要看到的那样,但最好不要重复计算出的信息。
为了表示应用的状态,我们推荐使用值类型:结构体或枚举。元组也完全可以接受,但遗憾的是,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进行实验,并习惯这种架构。最终,这将变得自然,你可以开始编写自己的数据结构来表示此类状态机,这在无数情况下都将非常有用。
将整个状态声明为Equatable可以帮助我们在未使用视图模型的情况下减少UI更新,但这并不是一个强烈的要求,还有其他方法可以避免这种情况,尽管我们仍然推荐这样做。如果可能,将状态声明为 Codable 对于记录、调试、崩溃报告、监控等情况很有用,并且如果有必要也建议这样做。
核心部分
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:)
。
什么是真实存储?
真实的存储是一个你希望在app运行过程中创建并保持存在的类,因为它的唯一职责是作为单向数据流生命周期的协调者。这也是为什么我们希望只有一个存储实例,所以你可以创建一个静态的单例实例,或者将其保存在AppDelegate中。如果你的app支持多个窗口并想要在这多个实例之间共享状态,请小心使用SceneDelegate。这就是为什么通常建议使用AppDelegate、单例或全局变量而不是SceneDelegate作为存储。在SwiftUI中,你可以在你的app协议中将存储作为一个Combine/StateObject
创建。
@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将提供一个协议(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比所需更频繁地刷新,而且还会增加错误发生的风险,使得视图层的代码更加复杂,最终导致模块化程度降低,视图与全局模型耦合。
然而,您不希望将状态拆分成多个部分,因为将它们集中在唯一的位置可以确保一致性。同时,您不希望多个不同的地方负责处理操作,因为这可能会引发竞争条件。真正的Store是唯一一个真正拥有全局状态并有效地处理操作的地方,这正是它应有的样子。
为了解决这两个问题,我们提供了一个StoreProjection
,它符合StoreType
协议,因此从所有目的来看,它表现得就像一个真正的store,但实际上它仅使用自定义的州和动作类型来投影真正的store,也就是说,它要么是您模型的一个子集(例如状态树中的一个分支),要么是一个完全不同的实体,比如视图状态。一个StoreProjection
有两个闭包,它允许它在全球状态和视图使用的状态之间转换动作和状态。这样,视图就只与全球模型的微小部分耦合,而不会与整个全局模型耦合,StoreProjection
中的闭包将负责提取/映射视图感兴趣的部分。这也提高了性能,因为视图只有当全局状态中的相关属性发生变化时才会刷新,而不是任何属性。从另一个方向来看,视图只能调度一组有限的动作,这些动作将由StoreProjection
中的闭包映射到全局动作。
可以从任何其他StoreType
创建Store投影,甚至可以从另一个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
发生变化时才会刷新,因为AppState
中的其他更改将被忽略,因为在ContentViewState
中没有映射到任何东西。此外,ContentViewAction
只有一个情况,即onAppear
,这是视图可以调度的唯一东西,它不知道最终这将启动一个定时器(AppAction.foo(.bar(.startTimer))
)。视图不应该了解领域逻辑,其动作应仅限于buttonTapped
、onAppear
、didScroll
、toggle(enabled: Bool)
和其他只暗示UI交互的名称。如何将其映射到应用动作是其他部分的职责,在我们的示例中,由ContentViewAction
本身负责,但它可以是表示层、视图模型层,或者您决定创建来组织代码的任何结构。
这种方法还使测试更容易,因为视图不包含任何逻辑,而投影转换是纯函数。

中间件
MiddlewareProtocol
是一种插件,或者是几个插件的组合,它被分配到应用的全球StoreType
管道中,以处理接收到的每个动作(InputActionType
),响应地执行副作用,并最终在过程中分发更多的动作(OutputActionType
)。它在处理传入动作时也可以访问最新的StateType
。
我们可以把中间件看作是一个对象,它将动作转换为同步或异步任务,并在这些副作用完成后创建更多的动作,同时也能在处理动作时检查当前状态。
动作(Action)是一种轻量级的结构,通常是一个枚举,它被分发到ActionHandler
(通常是StoreType
)中。
例如,类似于ReduxStoreProtocol
的存储会将到达的新动作放入队列,并将其提交到一个中间件管道。换句话说,MiddlewareProtocol
是一个处理动作的类,并且具有分发更多动作的能力,无论是立即还是异步任务回调之后。中间件也可以简单地忽略动作,或者它可以通过执行副作用来响应,例如将日志写入文件或通过网络,或者执行HTTP请求。对于这些异步任务,当它们完成后,中间件可以分发包含响应负载的新动作(例如一个JSON文件,一个电影数组,凭证等)。其他中间件将处理这些动作,或者甚至可能是将来的同一个中间件,或者也许有些Reducer
将使用这个动作来改变状态,因为MiddlewareProtocol
本身永远不会改变状态,只会读取它。
MiddlewareProtocol/handle(action:from:state:
会在Reducer之前被调用,因此如果在这个点读取状态,它仍然会是未改变的版本。在实现此函数时,预期返回一个IO
对象,它是一个闭包,可以在其中执行副作用和分发新动作。在这个闭包内部,状态将在reducers处理完当前动作之后具有新的值,因此如果你复制了旧状态,你可以比较它们,记录、审计、执行分析跟踪、遥测或与外部设备(如Apple Watch)同步状态。通过网络的远程调试也是中间件的极佳用途。
每个分发的动作都与它的动作来源一起到来,这是该动作的主要分发者。中间件可以访问文件名、行号、函数名以及有关负责创建和分发该动作的实体的其他信息,这是一个非常有用的调试信息,可以帮助开发人员跟踪信息如何在应用程序中流动。
理想情况下,MiddlewareProtocol
应该是一个小型且可重用的组件,仅处理一个非常有限的动作集,并且与其他小型中间件相结合以创建更复杂的应用。例如,同一个CoreLocation
中间件可以用于iOS应用、其扩展、Apple Watch扩展,甚至不同的应用,只要它们共享一些子动作树和子状态结构。
一些中间件的建议
- 运行计时器,周期性地获取外部资源或更新某些本地状态
- 订阅
CoreData
、Realm
、Firebase Realtime Database
或等效数据库变化
- 作为
CoreLocation
的委托,检查显著的地理位置变化或信标范围,并触发更新状态的动作
- 成为
HealthKit
代理以跟踪活动,或者甚至将其与 CoreLocation
观测相结合,以便跟踪活动路径
- 记录器、遥测、审计、分析追踪器、崩溃报告碎屑
- 监控或调试工具,例如外部应用程序从不同设备远程监控状态和操作
WatchConnectivity
同步,保持 iOS 和 watchOS 状态同步
- API 调用和其他“冷观察器”
- 网络可达性
- 应用程序中的导航(Redux 协调器模式)
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
。
在这种情况下,这个状态类型可以是一个子集,用于提升到全局状态以便与其他中间件一起组合,作用于应用程序的全局状态。请查阅提升以了解更多详细信息。
返回 I/O 和执行副作用
在其最重要的功能中,中间件预期将返回一个IO
对象,这是一个闭包,在其中执行副作用并可以分发新操作。
在某些情况下,我们可能不想在还原器之后执行任何副作用或运行任何代码,在这种情况下,函数可以返回一个简单的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,它只是根据您喜欢的响应式库,将Observable、Publisher或SignalProducer包装起来。唯一的要求是,您的响应式流的输出(元素)必须是DispatchedAction,错误必须是Never。DispatchedAction是一个结构体,拥有动作本身和分发器(动作源),因此它对动作是通用的,符合EffectMiddleware的OutputAction。错误必须是Never,因为中间件预期解决所有副作用,包括错误。因此,如果您想处理错误,您可以在中间件中处理它;如果您想向用户警告错误,您可以在您的响应式流中捕获错误,并将其转换为像.somethingWentWrong(messageToTheUser: String)
这样的动作以分发,然后再将其降低到AppState。
可选的EffectMiddleware还可以处理依赖项。这有助于在中间件中进行依赖注入。如果您的依赖项泛型参数是Void,则可以立即创建中间件而没有传递任何依赖项,但是处理动作时不能使用任何外部依赖。如果依赖项泛型参数具有某种类型或元组,则处理动作时可以使用它们,但是为了创建效果中间件,您将需要提供该类型或元组。
重要:依赖项仅在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
等。此外,您可以将任何Publisher、Observable或SignalProducer提升到Effect,只要它们匹配所需的泛型参数,您可以简单地使用.asEffect()
函数。

Reducer
Reducer
是包裹在单例容器中的纯函数,它接受一个动作和当前状态,计算新的状态。
MiddlewareProtocol
管道可以做两件事:分发出站动作和处理入站动作。但是它们不能改变应用程序状态。中间件对我们的应用程序的最新状态只有只读访问,但是当我们需要mutation时,我们使用MutableReduceFunction
函数
(ActionType, inout StateType) -> Void
它具有与旧的ReduceFunction
相同的语义(但性能更好)
(ActionType, StateType) -> StateType
它接受一个动作和当前状态(作为可变的inout),计算新状态并将其更改
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
该函数在整个缓存的 stated 中递归地减少所有动作,并且这对于每个新接收到的动作都是增量发生的。
重要的是理解reducer是一个同步操作,它计算出新的状态而不产生任何副作用(包括非明显的副作用,如创建Date()
,使用DispatchQueue或Locale.current
),所以永远不要向Reducer
结构体添加属性或调用任何外部函数。如果您想这么做,请创建中间件并从中分发带有日期或Locale的动作。
Reducer也负责保持状态的一致性,因此修改状态之前始终进行最终的检查是个好主意,例如检查必须一起更改的其他依赖属性。
一旦reducer函数执行,存储将以单个源-of-truth更新其最新的计算状态,并将其传播到所有订阅者,这些订阅者将对新状态做出反应,并更新视图等。
此函数被封装在结构体中以克服Swift的一些限制,例如,允许我们将多个reducer组合成一个(归约运算,其中两个或更多的reducer变成一个)或将reducer从局部类型提升到全局类型。
提升reducer的能力使我们能够编写细粒度的“子reducer”,它将仅处理状态和/或动作的一个子集,将其放置在不同的框架和模块中,并通过提供全局和局部状态和动作之间映射的方式将它们插入更大的状态和动作处理程序。有关更多信息,请参阅提升。
reducer的一个可能的实现可能是
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
。这样,编译器将更多地帮助您。
- 尽可能使动作和状态泛型参数专化。如果状态卷是更大状态的一部分,您应避免将整个大状态传递给此reducer。使其简短、简洁并专化,这也有助于防止出现
default
情况或需要重新分配此reducer永远不会更改的属性。
┌────────┐
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
─ ─ ─ ─ ─ ┘
投影和提升
商店投影
应用程序应只有一个真正的商店,持有单个真实来源。但是,我们可以“推导”这个商店到小子集,称为商店投影,它可以处理状态或动作树的较小部分,甚至可以处理完全不同类型的动作和状态,只要我们能够映射回原始商店类型。它不会存储任何内容,只为原始商店进行投影。例如,视图可以定义一个完全自定义的视图状态和视图动作,我们可以创建一个在此类型上工作的StoreProjection
,只要它背后有一个真正的商店,其状态和动作类型可以以某种方式映射到视图状态和视图动作类型。商店投影将负责转换这些实体。

在许多情况下,您可能不想让您的视图能够访问整个应用状态或调度任何可能的全球应用操作。这不仅可能导致UI比所需更频繁地刷新,而且还会增加错误发生的风险,使得视图层的代码更加复杂,最终导致模块化程度降低,视图与全局模型耦合。
然而,您不希望将状态拆分成多个部分,因为将它们集中在唯一的位置可以确保一致性。同时,您不希望多个不同的地方负责处理操作,因为这可能会引发竞争条件。真正的Store是唯一一个真正拥有全局状态并有效地处理操作的地方,这正是它应有的样子。
为了解决这两个问题,我们提供了一个StoreProjection
,它符合StoreType
协议,因此从所有目的来看,它表现得就像一个真正的store,但实际上它仅使用自定义的州和动作类型来投影真正的store,也就是说,它要么是您模型的一个子集(例如状态树中的一个分支),要么是一个完全不同的实体,比如视图状态。一个StoreProjection
有两个闭包,它允许它在全球状态和视图使用的状态之间转换动作和状态。这样,视图就只与全球模型的微小部分耦合,而不会与整个全局模型耦合,StoreProjection
中的闭包将负责提取/映射视图感兴趣的部分。这也提高了性能,因为视图只有当全局状态中的相关属性发生变化时才会刷新,而不是任何属性。从另一个方向来看,视图只能调度一组有限的动作,这些动作将由StoreProjection
中的闭包映射到全局动作。
可以从任何其他StoreType
创建Store投影,甚至可以从另一个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检查当前用户的位置以及从UserDefaults读取设置。
尽管这些操作组合在一起以创建完整体验,但我们仍然可以将它们彼此隔离,以避免在同一个地方有URLSession逻辑和CLLocation逻辑,这样做可能会导致资源竞争,并可能引发竞争条件。此外,单独测试这些部分通常更容易,并且有助于编写更有意义的测试。
理想情况下,我们应该将我们的AppState
和AppAction
组织为独立树,以考虑到这些部分。例如,在上面的例子中,我们可以在AppState中添加3个不同的属性,在AppAction中添加3个不同的枚举情况,以将有关天气API、用户位置和UserDefaults访问的相关状态和操作分组。
如果我们将应用分解为3种类型的Reducer
和3种类型的MiddlewareProtocol
,这会更加有用,而且每个类型都只针对我们模型中分组的3个路径而不是完整的AppState
和AppAction
进行操作。《Reducer》和《MiddlewareProtocol》的第一对将覆盖WeatherState
和《WeatherAction》,第二对将覆盖《LocationState》和《LocationAction》,第三对将覆盖《RepositoryState》和《RepositoryAction》。它们甚至可以位于不同的框架中,这样编译器就会禁止我们将天气API代码与CLLocation代码耦合,这对于强制更好的实践和代码的可重用性来说是非常好的。
但是,在某些时候,我们希望将这些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
的Reducer
,我们可以通过告诉这个Reducer
如何在全局树中找到它需要的属性,来“提升”它到全局类型AppAction
和AppState
。这将是\AppAction.weather
和\AppState.weather
。同样,也可以对中间件以及我们应用中的另外两个Reducer
和中间件这样做。
当所有这些都被提升到公共类型时,我们可以使用菱形运算符(<>
)将它们组合起来,并将其设置为存储处理器。
重要:由于Swift中的枚举没有键路径,如同struct,我们强烈建议您阅读操作枚举属性文档,并为每个情况实现属性,无论是手动完成还是使用代码生成器,以避免以后编写大量易出错的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并确保所有枚举情况都被棱镜覆盖**。
- 对于stateGetter闭包的类型2,找到从AppState解析到匹配类型的镜头(属性获取器)。
- 对于stateSetter闭包的类型2,找到可以从全局状态接收到的newValue的属性设置器镜头。务必保证一切可写。
使用KeyPath提升Reducer
.lift(
action: \AppAction.prism1?.prism2?.prism3,
state: \AppState.property1.property2
)
步骤
- 从上面的闭包示例开始
- 对于action,我们可以使用从通过棱镜树遍历的KeyPath。
- 对于state,我们可以使用WritableKeyPath从遍历属性,只要所有这些属性都被声明为var,而不是let。
提升Middleware
MiddlewareProtocol
有AppAction输入、AppAction输出和AppState输入,因为它可以处理动作、分发动作,并且只读取状态(永远不写入)。
因此,提升方向应该是
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 │ │
└───────────┘
│ │
使用闭包提升Middleware
.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包装它(假设"it" = 本地),然后在.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遍历属性。
可选转换
如果某个操作正在通过存储运行,一些reducer和中间件可能会选择忽略它。例如,如果操作树与那个中间件或reducer无关。这就是为什么,每个传入的动作(中间件的inputAction和简单的Action)都是从AppAction → Optional<Subset>
的转换。返回nil表示将忽略该操作。
对于相反的方向并不成立,当中间件分派操作时,它们必须成为AppAction,我们不能忽略中间件的观点。
箭头方向
Redizers接收动作(输入动作)并能够读写状态。
中间件接收动作(输入动作),分派动作(输出动作)并且只读取状态(输入状态)。
在提升时,我们必须牢记这一点,因为它定义了转换的变异性(协变/逆变),即map或contramap。
一个特殊情况是reducer的状态,因为这需要读写访问,换句话说,你得到一个inout Whole
和一个用于Part
的新值,你使用这个新值来设置inout Whole中的正确路径。这正是WritableKeyPaths的用途,我们现在将更详细地看到它。
KeyPaths的使用
KeyPath等同于Global -> Part
的转换,您可以使用以下方式描述树的结构:\Global.parent.part
。
WritableKeyPath具有类似的语法,但功能更强大,允许我们将(Global, Part) -> Global
或(inout Global, Part) -> Void
转换为相同的类型。
需要理解的是,KeyPaths仅在箭头方向从AppElement -> ReducerOrMiddlewareElement
时才可能,即
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
,我们不能使用KeyPaths,它没有意义,因为方向与我们的目标相反。在这种情况下,我们不是从一个全局值中解包/提取部分的值,而是从某个Middleware接收特定的action,我们需要将其封装在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作为挑战。这就是为什么这个函数被称为荒谬的,因为它无法被调用。
Xcode片段
// 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。你的控制器所要做的只是将视图操作转发到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已经包含了正确的依赖项。
然后,您必须安装您的pods,并打开.xcworkspace
文件而不是.xcodeproj
文件。
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")])
]
)
Combine,动态链接模式(使用类似的方法为 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 或更高版本。
重要:对于 Xcode 12,请使用版本 0.8.8。0.9.0 及以上版本需要 Xcode 13。
Carthage
由于缺乏兴趣和高维护成本,Carthage 已不再受支持。
如果这对此项服务至关重要,请提交一个 Github 问题并告知我们,我们将评估将其恢复的可能性。在此期间,您可以检查最后的兼容 Carthage 版本,该版本为 0.7.1,并最终在该版本上进行目标开发,直到我们找到更好的解决方案。