iOS 工程

[iOS 架构 #9] ReactorKit 总结:单向架构的标准

ReactorKit 是让 UIKit 与 RxSwift 组合采用单向数据流作为事实标准的框架。本文总结 View 和 Reactor 两种角色、需要 Mutation 中间阶段的原因,以及它与 TCA 的区别。

5 分钟阅读
[iOS 架构 #9] ReactorKit 总结:单向架构的标准 封面图

在 TCA(The Composable Architecture)篇中,我们介绍了单向数据流。事实上,中国内地之外的韩国 iOS 圈早已有一个更早将单向架构建立为标准的框架,那就是 ReactorKit。

这个由全秀烈(Suyeol Jeon)于 2017 年公开的框架,在 StyleShare 和 Kakao 系列服务采用后,成为 UIKit + RxSwift 组合的事实标准架构。甚至曾有一段时间,招聘信息中经常出现“有 ReactorKit 经验者优先”。

今天我们来梳理 ReactorKit 的结构、它与 TCA 的区别,以及它当前的定位。


View 和 Reactor,只有两个角色

ReactorKit 的结构很简单。每个页面都拆分为 View 和 Reactor。

  • View:将用户输入流向Action,并订阅State来绘制页面。不包含任何逻辑。
  • Reactor:接收 Action 并生成 State 的逻辑模块。它完全不了解 UI。

关键在于,Reactor 内部的数据流被固定为单一方向。

Action → mutate() → Mutation → reduce() → State → View

按钮点击以Action.refresh进入后,mutate()会处理网络请求等副作用,并发出Mutation.setItems([...])reduce()接收这个 Mutation 后计算新的 State。View 订阅 State,然后更新页面。

为什么需要 Mutation 这个中间阶段

如果你熟悉 MVVM(Model-View-ViewModel),可能会问:“为什么不直接从 Action 修改 State?”加入 Mutation 是为了隔离异步处理

mutate()是唯一允许执行异步工作的地方。相反,reduce()是纯函数,因此输入相同的 State 和 Mutation 时,总会得到相同结果。副作用被固定在一个位置,所以追踪状态为何变化时,也只需要查看一个地方。

final class SearchReactor: Reactor {
    enum Action { case updateQuery(String) }
    enum Mutation { case setResults([Repo]) }

    struct State { var results: [Repo] = [] }

    func mutate(action: Action) -> Observable<Mutation> {
        switch action {
        case .updateQuery(let query):
            return service.search(query)
                .map { Mutation.setResults($0) }
        }
    }

    func reduce(state: State, mutation: Mutation) -> State {
        var newState = state
        switch mutation {
        case .setResults(let repos): newState.results = repos
        }
        return newState
    }
}

Reactor 是不了解 UI 的纯逻辑,因此测试也很简单。传入 Action,再检查 State 即可。

ReactorKit Action mutate Mutation reduce State 单向流程图
异步处理隔离在 mutate() 中,纯计算隔离在 reduce() 中

它与 TCA 有什么不同

两者都是受 Redux 影响的单向架构,但应用单位不同。

ReactorKit TCA
基础 RxSwift Combine·Swift Concurrency
应用单位 每个页面(View)一个 Reactor 将功能级 Reducer 合成为树
引入方式 可以按页面逐步引入 将整个应用构建为一个体系
主要场景 UIKit SwiftUI

ReactorKit 的实用性来自它的引入方式。在现有 MVC(Model-View-Controller)项目中,只需选定一个页面并添加 Reactor 即可运行,不需要重写整个应用。相比之下,TCA 还会管理功能之间的状态组合,但需要整体采用这套体系。

另外,还有一个将 Redux 直接移植到 iOS 的库 ReSwift,但应用全局单一 store 的方式难以与 UIKit 配合,因此没有像 ReactorKit 那样普及。

当前的 ReactorKit

坦率地说,新项目选择 ReactorKit 的情况正在减少,因为它的弱点已经变得明显。

第一,它与 RxSwift 强耦合。随着 Apple 生态转向 Combine 和 Swift Concurrency,RxSwift 依赖本身成了负担。

第二,它与 SwiftUI 不匹配。View 协议和绑定方式都是以 UIKit 的命令式更新为前提设计的。

不过,ReactorKit 留下的遗产很明确。这个框架最早向韩国 iOS 开发者传递了“状态只沿一个方向流动”和“副作用隔离在一个地方”的理念。如果你现在正在学习 TCA,Action·Mutation·State 结构应该不会陌生;这种直觉很大一部分是在 ReactorKit 时代形成的。

如果你正在维护现有的 UIKit + RxSwift 代码库,ReactorKit 仍然是经过验证的选择。由于它按页面接入和移除,之后也可以逐个页面拆除并迁移,相对比较容易。

从 ReactorKit 向 TCA 传递单向流程接力棒的接力插画
单向性的理念由 ReactorKit 首先植入,之后由 TCA 继承并延续

总结

  • ReactorKit 是基于 RxSwift 的架构,将一个页面拆分为 View 和 Reactor,并强制采用 Action → Mutation → State 的单向流程。
  • 异步处理隔离在mutate()中,纯状态计算隔离在reduce()中。因此,追踪和测试都很容易。
  • 它与 TCA 的概念相同,但按页面逐步引入的实用路线是其差异所在。
  • 由于与 RxSwift 耦合且不兼容 SwiftUI,新项目中的引入有所减少,但在 UIKit 遗留代码中仍然有效。

下一篇将介绍 Uber 的 RIBs,它不是按页面,而是按业务逻辑单元拆分应用。我们会深入分析 VIPER 篇中只用一段提到的那个架构。


参考资料

延伸阅读