软件环境:Xcode 13.2、swift 5
创建时间:2022年 03月18号
适用范围:iOS项目
这篇文章都说了什么
痛点1,外部跳转很多,代码堆积。产品或者业务稍微丰富一点的时候,我们可能会借助推送来提高用户处理问题的及时性。所以,通过推送的消息很可能跳转好多页面,有原生的有H5,还要传递不同的参数。一般一点的处理方式可以是,通过某个参数,判断跳转到哪个页面,然后再分别取里面的各种参数,然后传值,跳转。可想而知,每增加一种消息,或者参数有变更,都要我们把控制跳转的部分做一次修改,一大堆if\else。
痛点2,内部模块之间依赖比较强,相互引用。项目大了的时候,如果要拆分开,很难做到。
通过本文的router,可以制定一套新规则,降低跳转的维护成本,模块之间没有强耦合。我们就围绕着适配以下场景来展开
1)外部跳转:如推送跳转、H5打开原生、其他app调起并跳转指定页面
2)内部跳转:模块解耦(模块间不强依赖,可单独存活)
3)支持一些回调,解决页面传值问题
还是先上一张图,整体看一下
即,主要分为2个子分支
1、先把目标页面注册到router中,如
// 我们可以给routerHelp做一个单例,然后实例一个可变字典,存储所有的路由,类似以下结构
open class HRouterHelper: NSObject {
@objc static let shared = HRouterHelper()
// 注意,swift的类名其实是项目名.类名
let mapper = NSMutableDictionary.init(dictionary: [
"app/webView": "project.SWWebViewController",
"app/404": "project.SW404ViewController",
"order/detail": "project.OrderDetailViewController",
"order/list": "project.OrderListViewController"
])
@objc public func rigisterPathes(pathes: [AnyHashable : Any]) {
mapper.addEntries(from: pathes)
}
}
2、跳转到页面OrderDetailViewController,并传一个orderId
viewControllerA.openViewController(path: "order/detail", query: ["orderId": 123])
上面openViewController(path: query:)是通过routerHelp给控制器做的扩展函数,调用时,通过path:order/detail到第一步的路由表中寻找目标页面,如果找到了,则实例它,并通过KVC把orderId传入到OrderDetailViewController的实例对象中。我们可以传常见的string、int、也可以传容器类属性、自定义类,还可以传闭包进去,用来接收回调。
传值的时候,有个问题得注意一下,如果query中有目标控制器中不存在的key,使用KVC会导致程序崩溃。所以可做以下处理,拦截异常。
override open func setValue(_ value: Any?, forUndefinedKey key: String) {
print("Set a undefined key:\(key) to instance of class:\(self.className())")
}
2.1 跳转的具体实现方式。
假设我们想控制跳转的方式,即通过导航push还是直接present,iOS11之后present是全屏还是半屏,等等很多细节。我们可以在query中定义一些特殊的参数,来告诉跳转中心以什么方式、什么样式跳转。类似以下控制
// extension UIViewController
public func openViewController(path: String, query: Dictionary, anyCallBack: @escaping AnyCallBack) {
// 通过path query获取目标页面的实例,这个函数会在下面提供的路由解析中提供
let controller = viewControllerWith(path: path, query: query)
// 是否全屏幕展示
let fullScreen = query[VCKEY_FULL_SEC, default: true] as! Bool
if fullScreen {
controller.modalPresentationStyle = .fullScreen
}
// 是否使用present方式弹出
let present = query[VCKEY_PRESENT, default: false] as! Bool
if present {
self.present(controller, animated: true, completion: nil)
} else if self.isKind(of: UINavigationController.self) {
// 控制器本身就是导航控制器
(self as! UINavigationController).pushViewController(controller, animated: true)
} else if self.navigationController != nil {
// 控制器有导航控制器
self.navigationController?.pushViewController(controller, animated: true)
} else {
// 找不到当行控制器,只能present了
self.present(controller, animated: true, completion: nil)
}
}
2.2 通过路由来寻址
// extension UIViewController
public func viewControllerWith(path: String, query: Dictionary) -> UIViewController {
// 这里的HRouterHelper.shared.mapper就是第一步说的那个路由表,我们做一个单例,来存储
// 路由表何时写入比较合适呢,建议app启动的时候就写入
if let vcName = HRouterHelper.shared.mapper[path] {
if NSClassFromString(vcName as! String) is UIViewController.Type {
let vcClass = NSClassFromString(vcName as! String) as! UIViewController.Type
let vcInstance = vcClass.init()
// KVC把参数传入
vcInstance.setValuesForKeys(query)
return vcInstance
} else {
return undefinedVC(path: path, query: query)
}
} else {
return undefinedVC(path: path, query: query)
}
}
// 兜底控制器,如果通过path没有找到控制器,就展示此页面,当然也可以直接toast提示错误
private func undefinedVC(path: String, query: Dictionary) -> UIViewController {
let url = path + query.description
let vc = viewControllerWith(path: "app/404", query: ["url": url])
return vc
}
比如有上面的订单详情,我们可以定义一个地址为:myScheme://myHost.com/order/detail?source=app&orderId=123
我们可以通过path(order/detail)找到控制器OrderDetailViewController
通过query(source=app&orderId=123)找到orderId = 123
也就是这个地址想跳转到订单123单详情页面
func openUrl(_ urlString: String) {
let url = NSURL.init(string: urlString)
let host = url?.host
let allowScheme = "myScheme"
let allowHost = "myHost.com"
let currentVC = topMostViewController()
if let scheme = url?.scheme {
// h5页面直接使用webView打开
if scheme.hasPrefix("http") {
currentVC.openViewController(path: "app/webView", query: ["urlString": urlString])
} else if scheme == allowScheme && host == "allowHost" {
var path = url?.path ?? ""
if path.hasPrefix("/") {
path = (path as NSString).replacingCharacters(in: NSMakeRange(0, 1), with: "")
}
let query = url?.query?.urlDecoded()
let param = getParamWithQuery(query!)
currentVC.openViewController(path: path, query: param as! Dictionary)
} else {
currentVC.openViewController(path: "app/404", query: ["url": urlString])
}
} else {
currentVC.openViewController(path: "app/404", query: ["url": urlString])
}
}
// 把query转换成字典 ,如source=app&orderId=123
func getParamWithQuery(_ query: String) -> NSDictionary {
let param = NSMutableDictionary.init()
let array = query.components(separatedBy: CharacterSet.init(charactersIn: "&"))
for str in array {
let array_in = str.components(separatedBy: CharacterSet.init(charactersIn: "="))
if array_in.count == 2 {
param.setValue(array_in[1], forKey: array_in[0])
}
}
return param
}
在2.1的代码中,我们注意到,有个anyCallBack的参数,在代码中其实没有用到。其实是我把其中的几行代码删掉了,拿到这里统一来讲,代码如下
if controller.isKind(of: SWBaseViewController.self) {
(controller as! SWBaseViewController).callBack = anyCallBack
}
其实,我所有的页面都有一个基类,用来做一些导航、回调、手势、埋点等控制。这里咱们要说的是使用router的时候,回调的处理。所以如果你有基控制器,建议在基控制器声明一个会调,这里直接赋值。当然,如果你把他装在query里面也可以。关于基控制,我们后面再做详细分享。相信整套框架组合下来,能提升一些中小型项目的开发效率。
用100多行代码,就解决了app的跳转问题
以后APP内部,我们可以这样跳
viewControllerA.openViewController(path: "order/detail", query: ["orderId": 123])
APP外部,我们可以这样跳
viewControllerA.openUrl("myScheme://myHost.com/order/detail?orderId=123")