2021-11-04

Coordinator

今天要讨论的是其中之一,即在解决「数据流问题」之后,再对视图层的 Navigator 进行解耦,所谓的「Flow Coordinators」。

什么是 Coordinator

Coordinator 是 Soroush Khanlou 在一次演讲中提出的模式,启发自 Application Controller Pattern

先来看看传统的作法到底存在什么问题。


func

tableView(_tableView:UITableView,didSelectRowAtindexPath:IndexPath) {

letitem=self.dataSource[indexPath.row]

letvc=DetailViewController(item.id)

self.navigationController.pushViewController(vc, animated:true, completion:nil)

}

再熟悉不过的场景:点击 ListViewController 中的 table 列表元素,之后跳转到具体的 DetailViewController。

实现思路即在 UITableViewDelegate的代理方法中实现两个 view 之间的跳转。

传统的耦合问题

看似很和谐。

好,现在我们的业务发展了,需要适配 iPad,交互发生了变化,我们打算使用 popover 来显示 detail 信息。

于是,代码又变成了这个样子:

functableView(_tableView:UITableView,didSelectRowAtindexPath:IndexPath) {

letitem=self.dataSource[indexPath.row]

letvc=DetailViewController(item.id)

if(!Device.isIPad()) {

self.navigationController.pushViewController(vc, animated:true, completion:nil)

}else{

varnc=UINavigationController(rootViewController: vc)

nc.modalPresentationStyle=UIModalPresentationStyle.Popover

varpopover=nc.popoverPresentationController

popoverContent.preferredContentSize=CGSizeMake(500,600)

popover.delegate=self

popover.sourceView=self.view

popover.sourceRect=CGRectMake(100,100,0,0)

presentViewController(nc, animated:true, completion:nil)

}

}

复制代码

很快我们感觉到不对劲,经过理性分析,发现以下问题:

view controller 之间高耦合

ListViewController 没有良好的复用性

过多 if 控制流代码

副作用导致难以测试

Coordinator 如何改进

显然,问题的关键在于「解耦」,看看所谓的 Coordinator 到底起到了什么作用。

先来看看 Coordinator 主要的职责:

为每个 ViewController 配置一个 Coordinator 对象

Coordinator 负责创建配置 ViewController 以及处理视图间的跳转

每个应用程序至少包含一个 Coordinator,可叫做 AppCoordinator 作为所有 Flow 的启动入口

了解了具体概念之后,我们用代码来实现一下吧。

不难看出,Coordinator 是一个简单的概念。因此,它并没有特别严格的实现标准,不同的人或 App 架构,在实现细节上也存在差别。

但主流的方式,最多是这两种:

通过抽象一个 BaseViewController 来内置 Coordinator 对象

通过 protocol 和 delegate 来建立 Coordinator 和 ViewController 之间的联系,前者对后者的「事件方法」进行实现

由于个人更倾向于低耦合的方案,所以接下来我们会采用第二种方案。

事实上 BaseViewController 在复杂的项目中,也未必是一种优秀的设计,不少文章采用 AOP 的思路进行过改良。

好了,首先我们定义一个 Coordinator 协议。

protocolCoordinator:class{

funcstart()

varchildCoordinators: [Coordinator] {getset}

}

复制代码

Coordinator 存储了「子 Coordinators」 的引用列表,防止它们被回收,实现相应的列表增减方法。

extensionCoordinator{

funcaddChildCoordinator(childCoordinator:Coordinator) {

self.childCoordinators.append(childCoordinator)

    }

funcremoveChildCoordinator(childCoordinator:Coordinator) {

self.childCoordinators=self.childCoordinators.filter {$0!==childCoordinator }

    }

}

复制代码

我们说过,每个程序的 Flow 入口是由 AppCoordinator 对象来启动的,在 AppDelegate.swift写入启动的代码.

funcapplication(_application:UIApplication,didFinishLaunchingWithOptionslaunchOptions: [UIApplicationLaunchOptionsKey:Any]?) ->Bool{

self.window=UIWindow(frame:UIScreen.main.bounds)

self.window?.rootViewController=UINavigationController()

self.appCoordinator=AppCoordinator(with: window?.rootViewControlleras!UINavigationController)

self.appCoordinator.start()


returntrue

}

复制代码

回到我们之前 ListViewController 的例子,我们重新梳理下,看看如何结合 Coordinator。假设需求如下:

如果用户未登录状态,显示登录视图

如果用户登录了,则显示主视图列表

定义 AppCoordinator 如下:

finalclassAppCoordinator:Coordinator{

fileprivateletnavigationController:UINavigationController

init(withnavigationController:UINavigationController) {

self.navigationController=navigationController

}

overridefuncstart() {

if(isLogined) {

showList()

}else{

showLogin()

}

}

}

复制代码

那么如何在 AppCoordinator 中创建和配置 view controller 呢?拿 LoginViewController 为例。

privatefuncshowLogin() {

letloginCoordinator=LoginCoordinator(navigationController:self.navigationController)

loginCoordinator.delegate=self

loginCoordinator.start()

self.childCoordinators.append(loginCoordinator)

}

extensionAppCoordinator:LoginCoordinatorDelegate{

funcdidLogin(coordinator:AuthenticationCoordinator) {

self.removeCoordinator(coordinator: coordinator)

self.showList()

    }

}

复制代码

再来看看如何定义 LoginCoordinator:

importUIKit

protocolLoginCoordinatorDelegate:class{

funcdidLogin(coordinator:LoginCoordinator)

}

finalclassLoginCoordinator:Coordinator{

weakvardelegate:LoginCoordinatorDelegate?

letnavigationController:UINavigationController

letloginViewController:LoginViewController

init(navigationController:UINavigationController) {

self.navigationController=navigationController

self.loginViewController=LoginViewController()

    }

overridefuncstart() {

self.showLogin()

    }

funcshowLogin() {

self.loginViewController.delegate=self

self.navigationController.show(self.loginViewController, sender:self)

    }

}

extensionLoginCoordinator:LoginViewControllerDelegate{

funcdidLogin() {

self.delegate?.didLogin(coordinator:self)

    }

}

复制代码

正如 UIKit 基于 delegate 的设计,我们靠这种方式真正实现了对 view controller 进行了解耦。

同理 LoginViewController 也存在相应的 LoginViewControllerDelegate 协议。

importUIKit

protocolLoginViewControllerDelegate:class{

funcdidLogin()

}

finalclassLoginViewController:UIViewController{

weakvardelegate:LoginViewControllerDelegate?

……

}

复制代码

这样,一套基本的 Coordinator 方案就出炉了。当然,目前还是非常基础的功能子集,我们完全可以在这个基础上扩展得更加强大。

适配多入口

显然,一个成熟的 App 会存在多样化的入口。除了我们一直在讨论的 App 内跳转之外,我们还会遇到以下的路由问题:

Deeplink

Push Notifications

Force Touch

常见的,我们很可能需要在手机上点击一个链接之后,直接链接到 app 内部的某个视图,而不是 app 正常打开时显示的主视图。

AndreyPanov 的方案解决了这个问题,我们需要对 Coordinator 再进行拓展。

protocolCoordinator:class{

funcstart()

funcstart(withoption:DeepLinkOption?)

varchildCoordinators: [Coordinator] {getset}

}

复制代码

增加了一个 DeepLinkOption? 类型的参数。这个有什么用呢?

我们可以在 AppDelegate 中针对不同的程序唤起方式都用 Coordinator 进行启动。

funcapplication(_application:UIApplication,didFinishLaunchingWithOptionslaunchOptions: [UIApplicationLaunchOptionsKey:Any]?) ->Bool{

letnotification=launchOptions?[.remoteNotification]as?[String:AnyObject]

letdeepLink=buildDeepLink(with: notification)

self.applicationCoordinator.start(with: deepLink)

returntrue

}

funcapplication(_application:UIApplication,didReceiveRemoteNotificationuserInfo: [AnyHashable:Any]) {

letdict=userInfoas?[String:AnyObject]

letdeepLink=buildDeepLink(with: dict)

self.applicationCoordinator.start(with: deepLink)

}

funcapplication(_application:UIApplication,continueuserActivity:NSUserActivity,restorationHandler:@escaping([Any]?) ->Void) ->Bool{

letdeepLink=buildDeepLink(with: userActivity)

self.applicationCoordinator.start(with: deepLink)

returntrue

}

复制代码

利用 buildDeepLink 方法对不同的入口方式判断输出相应的 flow 类型。

我们对之前的业务需求进行相应的扩展,假设存在以下三种不同的 flow 类型:

enumDeepLinkOption{

caselogin// 登录

casehelp// 帮助中心

casemain// 主视图

}

复制代码

我们来实现下 AppCoordinator 中的新 start 方法:

overridefuncstart(withoption:DeepLinkOption?) {

//通过 deeplink 启动

ifletoption=option {

switchoption {

case.login: runLoginFlow()

case.help: runHelpFlow()

default: childCoordinators.forEach { coordinatorin

            coordinator.start(with: option)

        }

        }

//默认启动

}else{

……

    }

}

复制代码

总结

本文专门介绍了 Coordinator 模式来对 iOS 开发中的 navigator 进行了深度的解耦。然而当今仍没有权威标准的解决方案,感兴趣的同学建议去 github 参考下其他更优秀的实践方案。


转载

https://blog.csdn.net/weixin_33768481/article/details/87940599

学习使用

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容