写在前面的话

在iOS开发中,架构设计总是非常重要而且令人头疼的部分之一。并且,其中的路由管理又是麻烦的一滩事。只要稍微忽视,路由代码就会到处都是,把你带入深不见底的迷宫(克苏鲁在召唤你┌(。Д。)┐)。

之前在阅读这个开源项目— Papr,这是一个非常优秀的个人开源项目。其中包含自己书写的路由代码架构,在一番研读与寻找之后,总算找到了其出处。配合RxSwift的响应式设计,是一种自己可以实现的,封装较好的路由管理架构,在此分享给大家。

读者预先需要学习RxSwift相关内容作为前置知识,来更好的理解这篇文章作者的想法。

Coordinator Pattern,干得好 🎉 🎉 🎉

作者:Côme Chevallier
原文链接:https://medium.com/smoke-swift-every-day/rxswift-coordinator-pattern-done-right-c8f123fdf2b2

总的来说

“关注点分离”(笔者:这是一个计算机科学概念)神圣不可侵犯,ViewController知道的是他们所绑定的ViewModel。在此基础上,如果要让ViewModel知道接下来应该跳转到哪个ViewController?该怎么做呢?

现在轮到Coordinator登场啦:它负责管理App中所有的跳转行为,不光是跳转的类型(Push,Present...),而且还要负责把VCs和VMs绑定在一起。

CoordinatorScene一起工作,这里引用下Florent Pillet的文章内容:

每个应用程序都是场景(Scene),由一对一对的ViewController的ViewModel组合在一起而成。ViewModel来控制当前要显示什么(除了第一个场景以外)。

Coordinator基本上来就是“上帝元素”,并且不含一点儿贬义:它把各部分组合在一起,给应用程序带来生命,但是各部分却不需要知道彼此的存在。

于是,Coordinator需要做这些事情:

  1. 展现Scene
  2. 于是,Scene由一对一对的VM和VC构成;
  3. 于是,VM和VC由一套绑定协议组合在一起;
  4. 于是,Coordinator通过路由协议,控制各个场景之间的跳转;
  5. 于是,Coordinator还定义了一些跳转的类型
  6. 于是,Coordinator实现了路由,并且在完成时发送一个通知。

什么东西不应该出现在Coordinator Pattern里?

Coordinator通过透传被注入进VM:

  1. 应该有且仅有一个Coordinator,所有的VM都指向它:所以Coordinator是一个Class也不是一个Struct;
  2. 除了VM的里的一个方法之外,其他任何地方都不要(其实应该是无法)访问Coordinator。还有仅仅一个例外,你需要在AppDelegate的didFinishLaunchingWithOptions中初始化他。

做一个鲁棒性极强的Coordinator,你需要准备这些

友情声明:Coordinator在本文中的实现,是由RW RxSwift Book的最后一章所启发的。
就像上面所说的,所以我会用6块代码来说明Coordinator到底是什么。

文件#1:Scene

Scene基本就是枚举,每个都对应的VM类型的变量。而ViewModel就是要展示给用户的内容,它便是大脑。
每次需要创建新的时候,就简简单单添加一个枚举值。

文件#2:Scene和(VM,VC)组合

身体照顾大脑,大脑控制身体。如果VM是大脑,那么VC显而易见就是身体。哪啊么,“上帝元素” — Coordinator是怎么通过调用viewController()方法,使它们协同工作的?

  • Coordinator内部的transition函数,接受一个Scene类型的参数(还记得它代表VM的枚举值吧?),函数必须知道VM绑定在了哪个VC上,这里使用switch语句即可。
  • 绑定本身也有多种多样的形式:firstScene的绑定,在你初始化路由栈,并跳转至rootVC时发生。secondScene更普通一些(至少看上去),但注意,它可能通过一个模态视图的形式,或者一个被push的VC的形式,甚至一个childVC的形式被呈现,也就是说,我们需要多种多样的跳转类型(文件#5和#6)。
  • VC作为函数的返回值的好处在于,你可以自定义展现形式(文件#6)。

那么bindViewModel(to:)方法又是做什么的呢?每个VC都需要通过Bindable协议实现这个方法。

File#3:绑定协议

所有的VC都需要实现BindableType,这意味着:

  1. 每个VC内部都有一个var viewModel变量,而它的类型由(VM,VC)组合来决定(注意:associatedtype只是一个占位符,用来创建一个泛型协议,你可以在任何地方调用。在运行时这个占位符会被在VC中声明的var viewModel的类型所替换);
  2. 每个VC都会实现bindViewModel()方法,这个方法用来设置VM的inputs,outputs和actions。

BindableType协议自己有一个默认的实现(在文件#2中使用),来处理VM和VC:由Scene的参数传过来的VM将会被赋值到var viewModel变量上,并且读取视图(如果还没被读取的话),所以初始UI将会被VM的output所更新,这也表示VM绑定到了VC上。

还有一个注意项:这个默认被实现的函数被标记为mutating,这是因为BindableType不一定应用于Class,所以能泛型一点就泛型一点。但奇怪的是,如果移除这个关键字的话,编译器将会有警告提示,但是我们明明已经在where中指定了一个引用类型。

文件#4:SceneCoordinatorType路由协议

这就是Coordinator将会实用的路由协议。

从上到下为:

  • Coordinator包含一个Window,在几乎所有的情况下,这就是主要的Window了,因为你的App只有一个Window(笔者注:作为弹窗实用的Window还是很多的,但是这里这么说也并没有错 ~);
  • Coordinator包含对当前VC的引用,第一个VC是默认值(只在初始化Coordinator时有意义),并且在AppDelegate里把它设置为window.rootViewController(在第一个.root跳转发生前)— 详情请查看文件#5和#6;
  • 在Coordinator中实现这些方法,这些方法中书写的就是在Scene之间跳转时需要做的事。

文件#5:跳转类型

SceneTransitionType也是枚举类型,其中罗列出的每一个枚举值都与跳转时的变量协同工作。

当你需要创建一个新的枚举值的时候,直接添加即可。

文件#6:SceneCoordinator:上帝元素

现在可以来出来看上帝了。

怎么使用它?

两个方式:初始化(在AppDelegate),和场景(Scene)跳转。

方式#1:初始化

Coordinator需要用一个带有rootVC的Window进行初始化,这就是为什么我们创建一个用.root跳转的方式来创建第一个场景,让它成为Window的rootVC。

方式#2:其他所有都长得像这样

最后,在VM中,你将会书写这样的代码(这里是代表Push):

为了能更好的理解这个例子,请参考ViewModel干得好🎉 🎉 🎉。要看看这些Action是怎么和ViewController连接起来的,请参考ViewController干得好🎉 🎉 🎉

进阶提升:

  1. pop方法也加到里面来;
  2. 其他的呢?