【QCon】精华内容上线92%,全面覆盖“人工智能+”的典型案例!>>> 了解详情
写点什么

基于 ReSwift 和 App Coordinator 的 iOS 架构

  • 2017-04-18
  • 本文字数:8042 字

    阅读完需:约 26 分钟

iOS 架构漫谈

当我们在谈 iOS 应用架构时,我们听到最多的是 MVC,MVVM,VIPER 这三个 Buzz Word,他们的逻辑一脉相承,不断的从 ViewController 中把逻辑拆分出去。从苹果官方推荐的 MVC:

图片来源

随着系统的复杂,把功能进行细化,把整合View 展示数据的逻辑的独立出来形成ViewModel 模块,架构风格就变成了MVVM:

图片来源

随着系统的更加复杂,把路由的职责,获取数据的职责也独立出去,架构风格就变成了VIPER:

图片来源

本文则想从另一个角度和大家探讨一个新的iOS 应用架构方案,架构的本质是管理复杂性,在讨论具体的架构方案前,我们首先应该明确一个iOS 应用的开发,其复杂性在哪里?

iOS 应用的开发复杂度

对于一个 iOS 应用来说,其开发的复杂性主要体现在三个方面:

复杂界面设计的实现和样式管理

iOS App 最终呈现给用户的是一组组的 UI 界面,而对于一个特定的 App 来说,其 UI 的设计元素(如配色,字体大小,间距等)基本上是固定的,另外,组成该 App 的基础组件(如 Button 种类,输入框种类等)也是有限的。但是如何管理,组合,重用组件则是架构师需要考虑的问题,尤其是一些 App 在开发过程中可能出现大量的 UI 样式重构,更需要清晰的控制住重构的影响范围。这儿的复杂性本质上是 UI 组件自身设计实现的复杂性,多 UI 组件之间的组合方式和 UI 组件的重用机制。

路由设计

对于一个大型的 iOS 应用,通常会把其功能按 Feature 拆分,经过这样的拆分之后,其可能出现的路由有以下几种:

  • APP 间路由: 从其它 App 调起当前 App,并进入一个很深层次的页面(图示 1)。

  • APP 内路由:

  1. 启动进入 App 的 Home 页面(图示 2)
  2. 从 Home 页面到进 Feature Flow(图示 3)
  3. Feature 内按流程的页面的路由(图示 4)
  4. 各 Feature 之间的页面跳转(图示 5)
  5. 各 Feature 共享的单点信息页的跳转(图示 6)

根据 Apple 官方的 MVC 架构,这些复杂的各种跳转逻辑,以及跳转前的 ViewController 的准备工作等逻辑缠绕在 AppDelegate 的初始化,ViewController 的 UI 逻辑中。这儿的复杂性主要是 UI 和业务之间缠绕不清的相互耦合。

应用状态管理

一个 iOS 应用本质上就是一个状态机,从一个状态的 UI 由 User Action 或者 API 调用返回的 Data Action 触发达到下一个状态的 UI。为了准确的控制应用功能,开发者需要能够清楚的知道:

  • 应用的当前 UI 是由哪些状态决定的?
  • User Action 会影响哪些应用状态?如何影响的?
  • Data Action 会影响哪些应用状态?如何影响的?

在 MVC,MVVM,VIPER 的架构中,应用的状态分散在 Model 或者 Entity 中,甚至有些状态直接保存在 View Controller 中,在跟踪状态时经常需要跨越多个 Model,很难获取到一个全貌的应用状态。另外,对于 Action 会如何影响应用的状态跟踪起来也比较困难,尤其是当一个 Action 产生的影响路径不同,或最终可能导致多个 Model 的状态发生改变时。这儿的复杂性主要体现在治理分散的状态,以及管理不统一的状态改变机制带来的复杂性。

如何管理这些复杂度

前面明确了 iOS 应用开发的复杂性所在,那么从架构层面上应该如何去管理这些复杂性呢?

使用 Atomic Design 和 Component Driven Development 管理界面开发的复杂度

UI 界面的复杂度本质上是一个点上的复杂度,其复杂性集中在系统的某些小细节处,不会增加系统整体规划的复杂度,所以控制其复杂度的主要方式是隔离,避免一个 UI 组件之间的相互交织,变成一个面上的复杂度,导致复杂度不可控。在 UI 层,最流行的隔离方式就是组件化,在笔者之前的一篇文章《前端组件化方案》中详细解释了前端组件化方案的实施细节,这儿就不再赘述。

使用App Coordinator 统一管理应用路由

应用的路由主要分为App 间路由和App 内路由,对它们需要分别处理

App 间路由

对于 APP 之间的路由,主要通过两种方式实现:

一种是 URL Scheme 通过在当前 App 中配置进行相应的设置,即可从别的 APP 跳转到当前 APP。进入当前 App 之后,直接在 AppDelegate 中的方法:

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool转换进 App 内的路由。

另一种是 Universal Links ,同样的通过在当前 App 中进行配置,当用户点击 URL 就会跳转到当前的 App 里。进入当前 APP 之后,直接在 AppDelegate 中的方法:

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool中转进 App 内路由。

所以 App 间的路由逻辑相对简单,就是一个把外部 URL 映射到内部路由中。这部分只需要增加一个 URL Scheme 或 Universal Link 对应到 App 内路由的处理逻辑即可。

App 内路由

对于内部路由,我们可以引入 App Coordinator 来管理所有路由。 App Coordinator 是 Soroush Khanlou 在 2015 年的 NSSpain 演讲上提出的一个模式,其本质上是 Martin Fowler 在《 Patterns of Enterprise Application Architecture 》中描述的 Application Controller 模式在 iOS 开发上的应用。其核心理念如下:

  1. 抽象出一个 Coordinator 对象概念
  2. 由该 Coordinator 对象负责 ViewController 的创建和配置。
  3. 由该 Coordinator 对象来管理所有的 ViewController 跳转
  4. Coordinator 可以派生子 Coordinator 来管理不同的 Feature Flow

经过这层抽象之后,一个复杂 App 的路由对应关系就会如下:

从图中可以看出,应用的 UI 和业务逻辑被清晰的拆分开,各自有了自己清晰的职责。ViewController 的初始化,ViewController 之间的链接逻辑全部都转移到 App Coordinator 的体系中去了,ViewController 则彻底变成了一个个独立的个体,其只负责:

  1. 自己界面内的子 UIView 组织,
  2. 接收数据并把数据绑定到对应的子 UIView 展示
  3. 把界面上的 user action 转换为业务上的 user intents,然后转入 App Coordinator 中进行业务处理。

通过引入 AppCoordinator 之后,UI 和业务逻辑被拆分开,各自处理自己负责的逻辑。在 iOS 应用中,路由的底层实现还是 UINavigationController 提供的 present,push,pop 等函数,在其之上,iOS 社区出了各种封装库来更好的封装 ViewController 之间的跳转接口,如 JLRoutes routable-ios MGJRouter 等,在这个基础上我们来进一步思考 App Coordinator,其概念核心是把 ViewController 跳转和业务逻辑一起抽象为 user intents(用户意图),对于开发者具体使用什么样的方式实现的跳转逻辑并没有限制,而路由的实现方式在一个应用中的影响范围非常广,切换路由的实现方式基本上就是一次全 App 的重构(做过 React 应用的 react-router0.13 升级的朋友应该深有体会)。所以在 App Coordinator 的基础之上,还可以引入 Protocol-Oriented Programming 的概念,在 App Coordinator 的具体实现和 ViewController 之间抽象一层 Protocols,把 UI 和业务逻辑的实现彻底抽离开。经过这层抽象之后,路由关系变化如下:

经过 App Coordinator 统一处理路由之后,App 可以得到如下好处:

  1. ViewController 变得非常简单,成为了一个概念清晰的,独立的 UI 组件。这极大的增加了其可复用性。
  2. UI 和业务逻辑的抽离也增加了业务代码的可复用性,在多屏时代,当你需要为当前应用增加一个 iPad 版本时,只需要重新做一套 iPad UI 对接到当前 iPhone 版的 App Coordinator 中就完成了。
  3. App Coordinator 定义与实现的分离,UI 和业务的分离让应用在做 A/B Testing 时变得更加容易,可以简单的使用不同实现的 Coordinator,或者不同版本的 ViewController 即可。

使用 Re S wift 管理应用状态

前面提到引入 App Coordinator 之后,ViewController 的剩下的职责之一就是“接收数据并把数据绑定到对应的子 UIView 展示”,这儿的数据来源就是应用的状态。它山之石,可以攻玉,不只是 iOS 应用有复杂状态管理的问题,在越来越多的逻辑往前端迁移的时代,所有的前端都面临着类似的问题,而目前 Web 前端最火的 Redux 就是为了解决这个问题诞生的状态管理机制,而 ReSwift 则把这套机制带入了 iOS 的世界。这套机制中主要有一下几个概念:

  • App State: 在一个时间点上,应用的所有状态. 只要 App State 一样,应用的展现就是一样的。
  • Store: 保存 App State 的对象,其还负责发送 Action 更新 App State.
  • Action: 表示一次改变应用状态的行为,其本身可以携带用以改变 App State 的数据。
  • Reducer: 一个接收当前 App State 和 Action,返回新的 App State 的小函数。

在这个机制下, 一个 App 的状态转换如下:

  • 启动初始化 App State -> 初始化 UI,并把它绑定到对应的 App State 的属性上
  • 业务操作 -> 产生 Action -> Reducer 接收 Action 和当前 App State 产生新的 AppState -> 更新当前 State -> 通知 UI AppState 有更新 -> UI 显示新的状态 -> 下一个业务操作…

在这个状态转换的过程中,需要注意,业务操作会有两类:

  • 无异步调用的操作,如点击界面把界面数据存储到 App State 上;这类操作处理起来非常简单,按照上面提到的状态转换流程走一圈即可。
  • 有异步调用的操作。如点击查询,调用 API,数据返回之后再存储到 App State 上。这类操作就需要引入一个新的逻辑概念( Action Creators ) 来处理,通过 Action Creators 来处理异步调用并分发新的 Action。

整个 App 的状态变换过程如下:

无异步调用操作的状态流转(图片来源

有异步调用操作的状态流转(图片来源

经过 ReSwift 统一管理应用状态之后,App 开发可以得到如下好处:

  1. 统一管理应用状态,包括统一的机制和唯一的状态容器,这让应用状态的改变更容易预测,也更容易调试。
  2. 清晰的逻辑拆分,清晰的代码组织方式,让团队的协作更加容易。
  3. 函数式的编程方式,每个组件都只做一件小事并且是独立的小函数,这增加了应用的可测试性。
  4. 单向数据流,数据驱动 UI 的编程方式。

整理后的 iOS 架构

经过上面的大篇幅介绍,下面我们就来归纳下结合了 App Coordinator 和 ReSwift 的一个 iOS App 的整体架构图:

架构实战

上面已经讲解了整体的架构原理,“Talk is cheap”, 接下来就以 Raywendlich 上面的这个 App 为例来看看如何实践这个架构。

(图片来源: https://koenig-media.raywenderlich.com/uploads/2015/03/PropertyFinder.png

第一步:构建 UI 组件

在构建 UI 组件时,因为每个组件都是独立的,所以团队可以并发的做多个 UI 页面,在做页面时,需要考虑:

  1. 该 ViewController 包含多少子 UIView?子 UIView 是如何组织在一起的?
  2. 该 ViewController 需要的数据及该数据的格式?
  3. 该 ViewController 需要支持哪些业务操作?

以第一个页面为例:

复制代码
class SearchSceneViewController: BaseViewController {
// 定义业务操作的接口
var searchSceneCoordinator:SearchSceneCoordinatorProtocol?
// 子组件
var searchView:SearchView?
// 该 UI 接收的数据结构
private func update(state: AppState) {
if let searchCriteria = state.property.searchCriter {
searchView?.update(searchCriteria: searchCriteria) } }?
// 支持的业务操作
func searchByCity(searchCriteria:SearchCriteria) {
searchSceneCoordinator?.searchByCity(searchCriteria: searchCriteria)
}?
func searchByCurrentLocation() {
searchSceneCoordinator?.searchByCurrentLocation()
}
// 子组件的组织
override func viewDidLoad() {
super.viewDidLoad()
searchView = SearchView(frame: self.view.bounds)
searchView?.goButtonOnClick = self.searchByCity
searchView?.locationButtonOnClick = self.searchByCurrentLocation
self.view.addSubview(searchView!)
}
}

注:子组件支持的操作都以 property 的形式从外部注入,组件内命名更组件化,不应包含业务含义。

其它的几个 ViewController 也依法炮制,完成所有 UI 组件,这步完成之后,我们就有了 App 的所有 UI 组件,以及 UI 支持的所有操作接口。下一步就是把他们串联起来,根据业务逻辑完成 User Journey。

第二步:构建 App Coordinators 串联所有的 ViewController

首先,在 AppDelegate 中加入 AppCoordinator,把路由跳转的逻辑转移到 AppCoordinator 中。

复制代码
var appCoordinator: AppCoordinator!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = UIWindow()
let rootVC = UINavigationController()
window?.rootViewController = rootVC
appCoordinator = AppCoordinator(rootVC)
appCoordinator.start()
window?.makeKeyAndVisible()
return true
}

然后,在 AppCoordinator 中实现首页 SeachSceneViewController 的加载

复制代码
class AppCoordinator {
var rootVC: UINavigationController
init(_ rootVC: UINavigationController){
self.rootVC = rootVC
}
func start() {
let searchVC = SearchSceneViewController();
let searchSceneCoordinator = SearchSceneCoordinator(self.rootVC)
searchVC.searchSceneCoordinator = searchSceneCoordinator
self.rootVC.pushViewController(searchVC, animated: true)
}
}

在上一步中我们已经为每个 ViewController 定义好对应的 CoordinatorProtocol,也会在这一步中实现

复制代码
protocol SearchSceneCoordinatorProtocol {
func searchByCity(searchCriteria:SearchCriteria)
func searchByCurrentLocation()
}
class SearchSceneCoordinator: AppCoordinator, SearchSceneCoordinatorProtocol {
func searchByCity(searchCriteria:SearchCriteria) {
self.pushSearchResultViewController()
}
func searchByCurrentLocation() {
self.pushSearchResultViewController()
}
private func pushSearchResultViewController() {
let searchResultVC = SearchResultSceneViewController();
let searchResultCoordinator = SearchResultsSceneCoordinator(self.rootVC)
searchResultVC.searchResultCoordinator = searchResultCoordinator
self.rootVC.pushViewController(searchResultVC, animated: true)
}
}

以同样的方式完成 SearchResultSceneCoordinator. 从上面的的代码中可以看出,我们跳转逻辑中只做了两件事:初始化 ViewController 和装配该 ViewController 对应的 Coordinator。这步完成之后,所有 UI 之间就已经按照业务逻辑串联起来了。下一步就是根据业务逻辑,让用 App State 在 UI 之间流转起来。

第三步:引入 ReSwift 架构构建 Redux 风格的应用状态管理机制

首先,跟着 Re S wift 官方指导选取你喜欢的方式引入 ReSwift 框架,笔者使用的是 Carthage。

定义 App State

然后,需要根据业务定义出整个 App 的 State,定义 State 的方式可以从业务上建模,也可以根据 UI 需求来建模,笔者偏向于从 UI 需求建模,这样的 State 更容易和 UI 进行绑定。在本例中主要的 State 有:

复制代码
struct AppState: StateType {
var property:PropertyState
...
}
struct PropertyState {
var searchCriteria:SearchCriteria?
var properties:[PropertyDetail]?
var selectedProperty:Int = -1
}
struct SearchCriteria {
let placeName:String?
let centerPoint:String?
}
struct PropertyDetail {
var title:String
...
}

定义好 State 的模型之后,接着就需要把 AppState 绑定到 Store 上,然后直接把 Store 以全局变量的形式添加到 AppDelegate 中。

复制代码
let mainStore = Store<AppState>(
reducer: AppReducer(),
state: nil
)

把 App State 绑定到对应的 UI 上

注入之后,就可以把 AppState 中的属性绑定到对应的 UI 上了,注意,接收数据绑定应该是每个页面的顶层 ViewController,其它的子 View 都应该只是以 property 的形式接收 ViewController 传递的值。绑定 AppState 需要做两件事:订阅 AppState

复制代码
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
mainStore.subscribe(self) { state in state }
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
mainStore.unsubscribe(self)
}

和实现 StoreSubscriber 的 newState 方法

复制代码
class SearchSceneViewController: StoreSubscriber {
......
override func newState(state: AppState) {
self.update(state: state)
super.newState(state: state)
}
......
}

经过绑定之后,每一次的 AppState 修改都会通知到 ViewController,ViewController 就可以根据 AppState 中的内容更新自己的 UI 了。

定义 Actions 和 Reducers 实现 App State 更新机制

绑定好 UI 和 AppState 之后,接下来就应该实现改变 AppState 的机制了,首先需要定义会改变 AppState 的 Action 们

复制代码
struct UpdateSearchCriteria: Action {
let searchCriteria:SearchCriteria
}
......

然后,在 AppCoordinator 中根据业务逻辑把对应的 Action 分发出去, 如果有异步请求,还需要使用 ActionCreator 来请求数据,然后再生成 Action 发送出去

复制代码
func searchProperties(searchCriteria: SearchCriteria, _ callback:(() -> Void)?) -> ActionCreator {
return { state, store in
store.dispatch(UpdateSearchCriteria(searchCriteria: searchCriteria))
self.propertyApi.findProperties(
searchCriteria: searchCriteria,
success: { (response) in
store.dispatch(UpdateProperties(response: response))
store.dispatch(EndLoading())
callback?()
},
failure: { (error) in
store.dispatch(EndLoading())
store.dispatch(SaveErrorMessage(errorMessage: (error?.localizedDescription)!))
}
)
return StartLoading()
}
}

Action 分发出去之后,初始化 Store 时注入的 Reducer 就会接收到相应的 Action,并根据自己的业务逻辑和当前 App State 的状态生成一个新的 App State

复制代码
func propertyReducer(_ state: PropertyState?, action: Action) -> PropertyState {
var state = state ?? PropertyState()
switch action {
case let action as UpdateSearchCriteria:
state.searchCriteria = action.searchCriteria
...
default:
break
}
return state
}

最终 Store 以 Reducer 生成的新 App State 替换掉老的 App State 完成了应用状态的更新。

以上三步就是一个完整的架构实践步骤,该示例的所有源代码可以在笔者的Github 上找到。

总结

以解决掉Massive ViewController 的iOS 应用架构之争持续多年,笔者也参与了公司内外的多场讨论,架构本无好坏,只是各自适应不同的上下文而已。本文中提到的架构方式使用了多种模式,它们各自解决了架构上的一些问题,但并不是一定要捆绑在一起使用,大家完全可以根据需要裁剪出自己需要的模式,希望本文中提到的架构模式能够给你带来一些启迪。


感谢张凯峰对本文的策划,徐川对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017-04-18 17:597385

评论

发布
暂无评论
发现更多内容

基于 std::variant 的运行时多态

SkyFire

c++ 多态

100万条数据解读2023银行APP用户体验升级方向

易观分析

金融 手机银行

浅谈 Java 多版本发布

lambochen

高效学 C++|继承与组合

TiAmo

c++ 编程语言 继承

DAPP/去中心化系统开发流程解析方案(成熟理念)分析结果

I8O28578624

C++命名空间:别再因为命名冲突而烦恼了。

子夜的星

c++ 命名空间 C++基础

编译期多态

SkyFire

c++ 多态

小令动态丨令牌云获中国金融科技·最佳新锐企业奖

令牌云数字身份

创新大赛 金融科技创新

NFT元宇宙链游游戏项目系统开发技术解析(Demo)

I8O28578624

CleanMyMac4.12.4最新版本有哪些新功能?

茶色酒

CleanMyMac CleanMyMac X2023

C++缺省参数:学习成为一名优秀的舔狗

子夜的星

c++ 缺省参数 编程语言、 基础语法

如何让Java编译器帮你写代码

京东科技开发者

后端 编译器 java; 编译器原理 企业号 1 月 PK 榜

佛萨奇2.0系统开发解析逻辑教程方案(成熟技术)

I8O28578624

一个小故事带你了解 Spring IoC

lambochen

【译】5 Different Ways to Create Objects in Java

lambochen

Pipy 0.90.0 发布

Flomesh

多线程并发 Pipy 流量管理

在可观测性的启蒙与初试探--快速实现根因分析/业务大盘

Yestodorrow

架构 监控 可观测性

嘉为蓝鲸IT服务管理解决方案入选2022广东省政务服务创新解决方案

嘉为蓝鲸

自动化运维 嘉为蓝鲸 IT服务管理中心

不过是享受了互联网的十年红利期而已。

why技术

Java 程序员 大学生

浅谈区块链项目开发技术(Solidity成熟语言)

I8O28578624

智能合约DAPP项目系统开发技术逻辑(dEOM)

I8O28578624

流程的价值一,固化业务的最佳实践!

CTO技术共享

流程的作用是服务于业务,所有不能被用来帮业务部门好好打粮食的流程,都不是好流程!

CTO技术共享

比Postman更懂中国程序员,Apipost真香!

不想敲代码

接口测试 API 研发管理工具

Mockito 助你实现真正的单元测试

lambochen

MySQL 主从备份实践

lambochen

Java踩坑之三目运算符类型转换

lambochen

Redis缓存的主要异常及解决方案

京东科技开发者

数据库 缓存 缓存击穿 Redis 数据结构 企业号 1 月 PK 榜

什么是NFT链游项目游戏系统开发技术(Demo)采用Solidity 智能合约系统开发方案

I8O28578624

人人都在聊的云原生数据库Serverless到底是什么?

华为云开发者联盟

数据库 Serverless 云原生 华为云 GaussDB

深入理解跨域和最佳实践分享

Crazy Urus

面试 前端 HTTP 跨域

基于ReSwift和App Coordinator的iOS架构_Android/iOS_刘先宁_InfoQ精选文章