Swift 路由方案升级

背景

大多 Swift 的路由都是基于原有 OC 的 Bus 和 URL 的方式继承过来的,但是随着主力语言切换到 Swift,以及在 Swift 中使用的越来越多,也越发的感觉到不【适应】。

类型转换问题

Swift 是一门强类型语言,更倾向于使用确定的类型参数,尽量避免使用 Any or AnyObject。相比之下,在 OC 语言中,从字典中获取字段后进行类型转换相对方便,只需使用 boolValue、intValue、floatValue 等方法即可实现(在不考虑类型安全的情况下)。然而,在 Swift 中,对于 Int、Bool、Float 等类型的转换,我们需要编写更多的额外代码来处理。 比如下面我们需要从字典中获取某个字段:

swift 复制代码
// 是否需要隐藏导航栏
var hideNavi = false
if let value = params?[WebHideNaviKey] as? String {
    // 传字符
    hideNavi = value == "1"
} else if let value = params?[WebHideNaviKey] as? Bool {
    // 传 Bool
    hideNavi = value
} else if let value = params?[WebHideNaviKey] as? Int {
    // 传 Int
    hideNavi = value == 1
}
Objc 复制代码
// 是否需要隐藏导航栏
BOOL hideNavi = [params[WebHideNaviKey] boolValue];

注册问题

我们都知道,使用 URL 的方式都需要在冷启的时候进行注册,通过注册 [String: String]以及 String -> Class 的映射,生成一个比较大的字典busObjectMap: [String: NSObject],最终被单例永久持有,用于后续的事件分发。

一方面生成实例对象是一个耗时操作,影响冷启耗时,随着版本迭代,模块越多对冷启的影响会越大。另一方面在 Swift 中,由于 Modules 的概念,注册的时候,value 值必须是 AppTarget.ClassName,如果后续做了组件化,value 值必须是 AppTarget.PodName.ClassName,这样的方式对后续的维护和扩展都有很大的影响。

为什么放弃 URL

先看下为什么一开始选择 URL 的方式做路由,很大一部分原因是 web 跳转原生页面以及三方跳转 App 都是通过 URL 的方式,无论最终选择任何路由,都避免不了对 URL 的解析处理。既然避免不了,那就统一,这样既可以处理 web 跳原生、三方跳转 App,还可以解决 App 内跳转导致的模块耦合问题。

那为什么放弃了 URL呢?上面提到了 URL 解决了两方面的问题,一、web和三方跳转原生页面,二、原生 App 之间的跳转,但实际上他们并不完全一致,甚至会有彼此冲突的地方。 比如:从图片列表跳转图片详情页,当前在列表页已经获取到图片数据 item

swift 复制代码
/* 这里假设图片无需存储本地 */

// 只考虑 App 内跳转,直接传图片
let detailVC = ImageDetailViewController(image: item.image)

// 考虑三方跳转的话,传图片地址
Bus.call("image/detail", params:[url: item.imageURLStr])

如果两种方案都存在,会导致维护成本成倍提升,而如果统一使用第二种方案的话,对 App 内跳转就会有以下问题:

  1. 已经拿到 Image 的前提下,由于第二种 case 的存在,只能传图片地址,图片需要二次下载,页面会有一个 loading 状态,用户体验会下降。
  2. 如果我们传图片的话,params 的类型只能是 [String: Any],接收端需要写大量的胶水代码来处理各种类型转换,这也太不 Swift 了。

此外,硬编码传参问题一直备受诟病,随着版本迭代,维护难度呈指数级增长。最终,我们决定放弃 URL 方案。

实践方案

基于以上背景,我们一直在思考一种能兼容多种情况的路由方案。 并没有,因为无论哪种方式,都绕不过对 URL 的处理,那么,就避免不了 URL 中各种字符串的解析。

但是我们想清楚了另外一件事,web 和三方跳原生只是原生内部跳转的一个子集,只是其中的一些特例,我们不应该因为特例而影响了对整个工程的设计,导致我们在使用过程中束手束脚。我们更多的心思还是要花在整个工程的架构设计和解耦上,因此产生了 Protocol Router 和 LinkHandler 两种处理方案。

Protocol Router

Protocol 的优势不仅在于约定了具体的参数类型,跳转 VC 的时候清晰的知道其所需要的参数传递,同时对外暴露的属性也可以放到 Protocol 里。 同一层级之间只需要在使用的时候,注入所依赖的 Protocol,当然也可以把所有的 Protocol Router 下沉,上层直接依赖。

swift 复制代码
// 协议声明
public protocol VIPRouter {
	func gotoPurchase(from type: PurchaseFromType)
}

// 具体的 module 来实现协议
class CEVIPModule: VIPRouter {
    func gotoPurchase(from type: PurchaseFromType) {
        let purchaseVC = CEPurchaseViewController(fromType: type)
        openVC(purchaseVC)
    }
}

// 依赖注入
class BaseFoundation {
    static var vipRouter: VIPRouter?
	static func registerVIPRouter(_ router: VIPRouter) {
    	self.vipRouter = router
    }
}

// 协议调用
class ModuleViewController {
	func buttonAction() {
    	BaseFoundation.vipRouter?.gotoPurchase(from: .cloud)
    }
}

LinkHandler

用于特殊处理 URL,只用于处理 web/三方跳转 App,针对具体的 URL 做好分类,然后分发到具体的 LinkModuleHandler,最终调用具体的 Protocol Router 来实现跳转。既避免了注册大字典,也不影响业务实现。

swift 复制代码
class LinkHandler {
    /// 假设 url 为 https://com.example/module/path?param=xxxx
	static func handleURL(_ url: URL) {
        
        // url 拆分成 module、path、参数字典
        let result = splitURL(url)
        let module = result.module
        let path = result.path
        let params = result.params
        if module == Module.vip { // 这里使用枚举会更好,为了减少代码,直接 if 判断了
        	LinkVIPHandler.handlePath(path, params: params)
        }
    }
}

class LinkVIPHandler {
	static func handlePath(_ path: String, params: [String: String]?) {
        if path == VIPPath.purchase { // 这里使用枚举会更好,为了减少代码,直接 if 判断了
            var fromType: PurchaseFromType = .none
            if let from = params["from"],
               let type = PurchaseFromType(rawValue: from) {
                fromType = type
            }
            BaseFoundation.vipRouter?.gotoPurchase(from: fromType)
        }
    }
}

小结

目前项目中已完全替换了以前的 Bus 方案,灰度全量了的 Router + Link 方案。在后续两个版本的使用过程中非常舒服,效率也有很大提升,跳转所需参数只需要查看协议即可清晰明了。再不用像以前一样,跳转需要先查看 Bus,再查看参数所需各种字符串依赖,再粘贴复制来避免字符串出错。

当然实践过程中我们也约定了一个规范,所有 VC 初始化依赖的参数,务必放初始化方法里面,尽可能少的通过属性来赋值,这一点很关键也很重要。

一家之言,欢迎拍砖。

相关推荐
依旧风轻7 小时前
Witness Table 的由来
ios·swift·witness table
依旧风轻7 小时前
Swift 中的方法调用机制
ios·swift·func·v-table·witness table
君陌笑1 天前
iOS-小说阅读器功能拆分之笔记划线
ios·swift
FreeCultureBoy2 天前
Swift 与 SwiftUI 学习系列:变量与常量篇 🚀
swiftui·xcode·swift
FreeCultureBoy2 天前
Swift 与 SwiftUI 学习系列: print 函数详解 🚀
swiftui·xcode·swift
依旧风轻2 天前
使用AES加密数据传输的iOS客户端实现方案
ios·swift·aes·network
依旧风轻3 天前
精确计算应用的冷启动耗时
ios·swift·cold start
hzgisme4 天前
iOS 视图实现渐变色背景
ios·cocoa·swift
大熊猫侯佩4 天前
Swift 中强大的 Key Paths(键路径)机制趣谈(上)
swift·序列·语法糖·键路径·keypath·转换为方法·协议扩展