写在前面的话
在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绑定在一起。
Coordinator和Scene一起工作,这里引用下Florent Pillet的文章内容:
每个应用程序都是场景(Scene),由一对一对的ViewController的ViewModel组合在一起而成。ViewModel来控制当前要显示什么(除了第一个场景以外)。
Coordinator基本上来就是“上帝元素”,并且不含一点儿贬义:它把各部分组合在一起,给应用程序带来生命,但是各部分却不需要知道彼此的存在。
于是,Coordinator需要做这些事情:
- 展现Scene;
- 于是,Scene由一对一对的VM和VC构成;
- 于是,VM和VC由一套绑定协议组合在一起;
- 于是,Coordinator通过路由协议,控制各个场景之间的跳转;
- 于是,Coordinator还定义了一些跳转的类型;
- 于是,Coordinator实现了路由,并且在完成时发送一个通知。
什么东西不应该出现在Coordinator Pattern里?
Coordinator通过透传被注入进VM:
- 应该有且仅有一个Coordinator,所有的VM都指向它:所以Coordinator是一个Class也不是一个Struct;
- 除了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,这意味着:
- 每个VC内部都有一个var viewModel变量,而它的类型由(VM,VC)组合来决定(注意:associatedtype只是一个占位符,用来创建一个泛型协议,你可以在任何地方调用。在运行时这个占位符会被在VC中声明的var viewModel的类型所替换);
- 每个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干得好🎉 🎉 🎉。
进阶提升:
- 把pop方法也加到里面来;
- 其他的呢?
评论