swift-oc混编分享3-UrlRouter跳转中心

软件环境:Xcode 13.2、swift 5
创建时间:2022年 03月18号
适用范围:iOS项目

这篇文章都说了什么

  1. 在iOS中,如何通过URL跳转到指定的页面,并传递一些参数
  2. 源码分享(swift),快速在你的项目中搭建跳转中心

场景:

        痛点1,外部跳转很多,代码堆积。产品或者业务稍微丰富一点的时候,我们可能会借助推送来提高用户处理问题的及时性。所以,通过推送的消息很可能跳转好多页面,有原生的有H5,还要传递不同的参数。一般一点的处理方式可以是,通过某个参数,判断跳转到哪个页面,然后再分别取里面的各种参数,然后传值,跳转。可想而知,每增加一种消息,或者参数有变更,都要我们把控制跳转的部分做一次修改,一大堆if\else。

        痛点2,内部模块之间依赖比较强,相互引用。项目大了的时候,如果要拆分开,很难做到。

        通过本文的router,可以制定一套新规则,降低跳转的维护成本,模块之间没有强耦合。我们就围绕着适配以下场景来展开

1)外部跳转:如推送跳转、H5打开原生、其他app调起并跳转指定页面
2)内部跳转:模块解耦(模块间不强依赖,可单独存活)
3)支持一些回调,解决页面传值问题


原理概览

还是先上一张图,整体看一下

swift-oc混编分享3-UrlRouter跳转中心_第1张图片

即,主要分为2个子分支

  1. 如果是内部的一些跳转,我们就直接使用path找到对应的控制器,然后传参,跳转;
  2. 如果是外部过来的,比如推送,那我们就先解析跳转地址,获取到path、query,然后走内部跳转的逻辑。如果要对外部跳转做鉴权,可以在解析url的时候,加一些规则、验证签名、或者配置可跳转的白名单等

具体介绍

内部跳转到某个页面,都要做哪些事

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
}

外部跳转url解析

 比如有上面的订单详情,我们可以定义一个地址为: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")

你可能感兴趣的:(iOS,swift,ios,xcode)