前几天我们 SwiftGG 激烈的讨论了 iOS 中 Router 。
于是今天用文字总结一下我个人的一些观点,并放出了 Demo 。注意,虽然有 Demo ,但仍然只是个思想,具体怎么实现,都可以。
我认为就是通过打开 指定的 URL (字符串),完成 对应的操作 ,通常该操作都是展示一个新的 ViewController 。
原则上和其他 APP 交互的方式只能是通过 URL Scheme ,事实上像微信分享、支付等 APP 交互都是通过 URL Scheme 的方式,具体的交互逻辑可以参考 MonkeyKing 。
此处微信分享等交互式通过 URL 和 UIPasteboard 进行的共享数据,当然大多数的 APP 都是采用这样的方式。
有一个良好的 URL Scheme 可以为使用者提供更多方便的交互,做的比较好的比如 OmniFocus 。
业界存在一个公认的 x-callback-url 标准,可以参见 http://x-callback-url.com/specifications/ 。
当产品提出这样的需求时,添加一个还好,添加多个就显得麻烦了,我们需要一种类似于后端的 router 方案,对应不同的 url ,匹配到不同的逻辑。
在目前的 App 中,这个场景还是很常见,比如打开了高德地图的 web ,我们想进行更复杂的交互,或者是获得更好的体验,就要打开 app 完成这件事情,于是这里总不能再去复制地址,打开搜索,粘贴吧。我们需要一个合理的方案可以直接从 web 中跳转到 app 。
移除 ViewController 中的依赖关系。
我表示,对于那些 Router 实现,我很不满意,基本上都不能满足我的需求,在此就不一一列举了。
所以在处理 Router 时,我们只需要 GET 一个比较合理的 URL 解析方案,这里我选择 ♂ 了 RouterX 。
本文重点讨论 根据解析结果做不同的处理 。
不可避免的是,我们仍然需要有个 ViewController 去调用以下几种方法展示新界面。
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 。
实现起来并不麻烦。
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 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
,表示为搜索界面。
var routingPattern: String {
return "/search/:text"
}
根据搜索内容判断是否为相同界面。
var routingIdentifier: String? {
return searchBar?.text
}
get
的实现就比较有意思了。一步一步来。
func get(url: NSURL, sender: JSON?) {
_searchText = sender?["text"].string
Router.topViewControler?.showDetailViewController(self, sender: nil)
}
赋值,通过查找 topViewController
,调用 showDetailViewController
展示自己。这里我们将赋值的逻辑和展示的逻辑全部交给了 SearchViewController
可能需求还不够,我们不希望当前界面是搜索界面,然后还去打开一个搜索界面,这个就非常尴尬了,也是没有必要的。
加一个验证就可以了。
if let topRouter = Router.topRouter where topRouter.routingIdentifier == routingIdentifier {
print("打开了完全一样的搜索界面")
return
}
判断当前的 topRouter
( topViewController
) 的 routingIdentifier
是不是相同的。如果是,直接返回就行了。
还不够,既然打开的已经是搜索界面了,那为什么不能更新一下搜索内容呢?
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
方法为。
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
中加入如下方法即可。
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 去做。