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

本文介绍了如何在iOS项目中通过URL路由实现页面的统一管理,包括内部和外部跳转,参数传递,回调处理,以及URL解析。通过创建一个路由中心,可以简化代码,减少模块间的耦合,提高开发效率。文章提供了具体的实现代码,包括页面注册、寻址、跳转方式控制、外部URL解析和事件回调的处理。
摘要由CSDN通过智能技术生成

软件环境:Xcode 13.2、swift 5
创建时间:2022年 03月18日
更新时间:2023年10月17日
适用范围:iOS项目

这篇文章都说了什么

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

场景:

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

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

        3、不同场景下跳转逻辑不同(push/present),不同场景下跳入页面,关闭动作不同(pop/dismiss)

        通过本文的router,可以统一管理页面的跳转,降低页面跳转的维护成本,模块之间没有强耦合。接下来我们就围绕着适配这些场景来展开

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


原理概览

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

即,主要分为2个子分支

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

具体介绍

1、页面注册

// 我们可以给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、通过路由来寻址

// extension UIViewController
public func viewControllerWith(path: String, query: Dictionary<String, Any>) -> 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<String, Any>) -> UIViewController {
    let url = path + query.description
    let vc = viewControllerWith(path: "app/404", query: ["url": url])
    return vc
}

  3、跳转

        假设我们想控制跳转的方式,即通过导航push还是直接present、iOS11之后present是全屏还是半屏、是否需要动画等。我们可以在query中定义一些特殊的参数,来告诉跳转中心以什么方式、什么动画跳转。类似以下控制

// extension UIViewController

public func openViewController(path: String, query: Dictionary<String, Any>, 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)
    }
}

4、举例跳转到某个页面

示例:跳转到页面OrderDetailViewController,并传一个orderId

viewControllerA.openViewController(path: "order/detail", query: ["orderId": 123])

        上面openViewController(path: query:)是通过routerHelp给控制器做的扩展函数,调用时,通过path:order/detail到第一步的路由表中寻找目标页面,如果找到了,则实例它,并通过KVC把orderId传入到OrderDetailViewController的实例对象中。我们可以传常见的string、int、也可以传容器类属性、自定义类,还可以传闭包进去,用来接收回调。

传值的时候,有几个问题得注意一下。

        1)swift类中需要通过这种方式接参的属性得用@Objc修饰,否则接收不到KVC消息;
        2)如果query中有目标控制器中不存在的key,使用KVC会导致程序崩溃。测试期间,严查参数的同时,还以可做以下处理,拦截异常,避免误传参数导致崩溃。

override open func setValue(_ value: Any?, forUndefinedKey key: String) {
    print("Set a undefined key:\(key) to instance of class:\(self.className())")
}

 5、外部跳转url解析

 比如有上面的订单详情,我们可以定义一个地址为:myScheme://myHost.com/order/detail?source=app&orderId=123的跳转链接。在消息中心、推送、H5页面植入。Android其实也有类似的组件,这样就可以让服务控制前端的跳转了。

我们可以通过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<String, String>)
            
        } 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
}

6、关于事件回调

在3的代码中,我们注意到,有个anyCallBack的参数,在代码中并没有用到。这是因为在示例中我把其中的几行代码删掉了,在这里统一来讲,删掉的代码如下

if controller.isKind(of: SWBaseViewController.self) {
    (controller as! SWBaseViewController).callBack = anyCallBack
}

        这个baseViewController是我项目中所有页面的基类,用来做一些导航、回调、手势、埋点等控制。这里咱们要说的是使用router的时候,回调的处理。那么如果你有基控制器,建议在基控制器声明一个通用的回调,这里直接赋值。当然,如果你把他装在query里面也可以,只不过需要回调的页面都需要向外公开一个回调属性。还需要注意的是,如果你想在OC中跳到swift,回调是不通用的,需要在跳转中心做一次翻译。

        关于基控制器,我们后面再做详细分享。相信整套框架组合下来,能提升一些中小型项目的开发效率。


总结:

用100多行代码,就解决了app的跳转问题

以后APP内部,我们可以这样跳

viewControllerA.openViewController(path: "order/detail", query: ["orderId": 123])

APP外部,我们可以这样跳

viewControllerA.openUrl("myScheme://myHost.com/order/detail?orderId=123")

 这种方式我最早用的是OC的实现方式,原理类似,比swift的打磨时间要长的多,功能也丰富。想参考的同学可以私信我。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值