前几天我们 SwiftGG 激烈的讨论了 iOS 中 Router 。

于是今天用文字总结一下我个人的一些观点,并放出了 Demo 。注意,虽然有 Demo ,但仍然只是个思想,具体怎么实现,都可以。

什么是 Router

我认为就是通过打开指定的 URL(字符串),完成对应的操作,通常该操作都是展示一个新的 ViewController 。

为什么需要 URL Router

和其他 APP 通信/交互

原则上和其他 APP 交互的方式只能是通过 URL Scheme ,事实上像微信分享、支付等 APP 交互都是通过 URL Scheme 的方式,具体的交互逻辑可以参考 MonkeyKing

此处微信分享等交互式通过 URL 和 UIPasteboard 进行的共享数据,当然大多数的 APP 都是采用这样的方式。

有一个良好的 URL Scheme 可以为使用者提供更多方便的交互,做的比较好的比如 OmniFocus

业界存在一个公认的 x-callback-url 标准,可以参见 http://x-callback-url.com/specifications/

当产品提出这样的需求时,添加一个还好,添加多个就显得麻烦了,我们需要一种类似于后端的 router 方案,对应不同的 url ,匹配到不同的逻辑。

从 Web 直接打开 APP

在目前的 App 中,这个场景还是很常见,比如打开了高德地图的 web ,我们想进行更复杂的交互,或者是获得更好的体验,就要打开 app 完成这件事情,于是这里总不能再去复制地址,打开搜索,粘贴吧。我们需要一个合理的方案可以直接从 web 中跳转到 app 。

解耦

移除 ViewController 中的依赖关系。

GitHub 上的 Router 资源

我表示,对于那些 Router 实现,我很不满意,基本上都不能满足我的需求,在此就不一一列举了。

Router 原理

事实上,Router 的原理很简单,只有两步。

  1. 解析 URL
  2. 根据解析结果做不同的处理

所以在处理 Router 时,我们只需要 GET 一个比较合理的 URL 解析方案,这里我选择 ♂ 了 RouterX

本文重点讨论 根据解析结果做不同的处理

TopViewController

不可避免的是,我们仍然需要有个 ViewController 去调用以下几种方法展示新界面。

1
2
3
4
func pushViewController(viewController: UIViewController, animated: Bool)
func presentViewController(viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?)
func showViewController(vc: UIViewController, sender: AnyObject?)
func showDetailViewController(vc: UIViewController, sender: AnyObject?)

这里就可能存在依赖关系,传入的参数是 ViewController ,我们需要实例化要展示的 ViewController 。比如我们有个 AViewController ,在 A 中展示 BViewController ,总是容易在 AViewController 写一几行代码去实例化 B ,并传入一些参数,再调用上面的转场方法。

那么,能不能把这些逻辑代码尽可能移到 B 中呢?可以的。

首先我们需要一个 topViewController 。所谓 topViewController 就是最上层的 ViewController ,即你在屏幕上看到的界面对应的 ViewController 。

实现起来并不麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func topViewController(base: UIViewController? = UIApplication.sharedApplication().keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(presented)
}
return base
}

这里不在赘述相应的逻辑了。

进入本文重点。

抽象的 protocol

1
2
3
4
5
6
7
8
9
10
protocol Routerable {
/// path , 用于判断是否为相同类型界面
var routingPattern: String { get }
/// unique path , 用于判断是否为完全相同界面
var routingIdentifier: String? { get }
/// GET 方法,一般用来展示新界面
func get(url: NSURL, sender: JSON?)
/// POST 方法,一般用来更新数据
func post(url: NSURL, sender: JSON?)
}

这就是本文最重要的几行代码了。

  • routingPattern 用于判断是否为相同类型界面,比如两个搜索界面,虽然一个搜索内容是 foo ,另一个是 bar ,但同样都是搜索,可以认为是相同的。一般认为一个 ViewController 是对应一个 routingPattern
  • routingIdentifier 用于判断是否为完全相同界面,仍然是搜索界面,一个搜索内容是 foo ,一个是 bar ,虽然同样是搜索,但搜索内容不同,故认为是不完全相同界面。这很有用。
  • get 一般用于展示界面,具体使用姿势见下文。
  • post 一般用于更新界面,具体使用姿势见下文。

当然定义的协议 Routerable 中协议约定只是一个参考,正因为是一个参考,我们可以做很多事情,自由性会很高,

以一个搜索界面为例。

定义 Pattern 为 /search ,表示为搜索界面。

1
2
3
var routingPattern: String {
return "/search/:text"
}

根据搜索内容判断是否为相同界面。

1
2
3
var routingIdentifier: String? {
return searchBar?.text
}

get 的实现就比较有意思了。一步一步来。

1
2
3
4
func get(url: NSURL, sender: JSON?) {
_searchText = sender?["text"].string
Router.topViewControler?.showDetailViewController(self, sender: nil)
}

赋值,通过查找 topViewController ,调用 showDetailViewController 展示自己。这里我们将赋值的逻辑和展示的逻辑全部交给了 SearchViewController

可能需求还不够,我们不希望当前界面是搜索界面,然后还去打开一个搜索界面,这个就非常尴尬了,也是没有必要的。

加一个验证就可以了。

1
2
3
4
if let topRouter = Router.topRouter where topRouter.routingIdentifier == routingIdentifier {
print("打开了完全一样的搜索界面")
return
}

判断当前的 topRouter(topViewController) 的 routingIdentifier 是不是相同的。如果是,直接返回就行了。

还不够,既然打开的已经是搜索界面了,那为什么不能更新一下搜索内容呢?

1
2
3
4
5
6
let searchText = sender?["text"].string
if let topRouter = Router.topRouter where topRouter.routingPattern == searchText {
print("仍然打开了搜索界面,这里不展示新界面,更新搜索内容")
topRouter.post(NSURL(string: "")!, sender: JSON(["text": searchText]))
return
}

topRouter 发送一个更新的信息。

对应 post 方法为。

1
2
3
func post(url: NSURL, sender: JSON?) {
searchBar.text = sender?["text"].string
}

这样一来就完成了内容的更新。

topRouter.post(NSURL(string: "")!, sender: JSON(["text": searchText])) 这段代码写起来还是比较尴尬的,有更优雅一些的用法,我写了一个 findRouterable 的方法供参考。(毕竟本文只是提供一些思路/思想

上面的代码需要注意的地方是。在 get 方法中拿不到 searchBar ,毕竟是用 Storyboard 创建的,此时 searchBar 还是个 nil ,所以我添加了一个 _searchText 的属性解决该问题。当然,还有其他的方法,比如代码布局(这并不麻烦)。

对于 demo ,你可以尝试不同场景下,在 Safari 中打开链接 router://qing.com/search/Foo ,体验上述逻辑效果。

登录验证

比较好玩的是,这里我们可以很轻松的完成对于登录的处理,比如 Timeline ,需要登录才能进入该界面,在 get 中加入如下方法即可。

1
2
3
4
5
if (未登录) {
// 跳转到登录
} else {
// 展示 Timeline
}

这很方便,我们将打开 timeline 的权限交给了它自己,这样就不需要再每次其他 ViewController 进行跳转时再去写判断逻辑什么的了。

补充

需要补充的是,Demo 中的代码可以写的很优雅,当然这不是本文的重点,如何将代码写的更优雅可以参考写更优雅的 Swift 框架 — rx_tap -> rx.tap 以及 写更优雅的 Swift 框架 - 续

复杂数据

其实这才是 Router 中比较痛苦的事情,传递一个非常大的 JSON 数据。这里就不推荐使用 url 传值了。比较简单的办法就是通过全局变量 var json = JSON.null 传递值。当然你也可以选择自己喜欢的方式。

限定字段

使用 Router 很难避免的就是 String ,到处都是 String 。比较好的方案就是将 String 换成强类型,用 func enum 都可以,Demo 中选择了 enum 。你可以将需要的值全部写到参数中,这样可以一定程度上减少传值时字段写错的问题。

域名

这个就非常有意思了,你甚至可以在 app 中定义各种域名,根据不同域名走不同逻辑等等。比如 qing.com xiaoqing.com ,会进行不同的匹配逻辑。

总结

总之,思想核心就是对于一个 ViewController ,所有的事情都尽可能的交给这个 ViewController 去做。