Swift基于枚举的路由设计实践

最近在作一个基于Swift的路由设计,其实我在刚开始接触Swift的时候就以为Swift的枚举很适合作路由。比方说相似于下面这样的写法:git

Router.pushTo(.user(.profile(userID: "Jake")), from: self, animated: true)

let loginVC = Router.viewControllerWithPath(.user(.login))
复制代码

枚举的嵌套设计、容许带参数,且能够明确参数是否可选,可让模块间调用更为可靠。那么要如何实如今如今多人合做和组件化项目的场景中呢。github

固然在这以前我仍是那句话,没有最好的方案,只有最合适的方案。json

Router与组件化

组件化的问题在于组件间的依赖关系,比方说界面须要依赖Router跳转,而Router须要依赖组件去建立界面,这样形成了模块间的循环引用。因此,Router不可以依赖子模块,咱们须要采用NSClassFromString的方式建立对象。bash

固然咱们须要用协议去约束这个Class和利用这个Class处理对应的路由路径操做。协议相似于下面这段代码,这里的协议设计也能够比较容易的去适配Objective-C写的模块。组件化

public protocol Routable: NSObject {
    static func viewControllerWith(routePath: RouterProtocol.Path) -> UIViewController?
}
复制代码

路由表设计

得益于Swift强大的枚举,咱们能够像这样声名咱们的路由路径优化

public struct RouterProtocol {

    public enum Path {
    
        public enum User {
            case login
            case profile(userID: String)
        }
        
        case user(User)
        case search
    }
}
复制代码

接下来咱们的Router须要一个路由表文件,这里我为了方便就使用了json,能够根据实际项目和我的习惯选择其余文件方式,那么咱们的路由表文件能够是这样的:ui

{
    "login":{
        "class":"UserModule.LoginViewModel"
    },
    "userProfile":{
        "class":"UserModule.UserViewModel"
    },
    "search":{
        "class":"SearchModule.SearchViewModel"
    }
}
复制代码

路由表解析

如今咱们能够根据咱们的枚举和路由表对应的字段,写出解析了。编码

实际上能够将路由表的解析方法利用代理的方式交给主项目解析,由主项目决定,这样能够进一步减小耦合。url

public struct RouteTarget {
    public let className: String
    public let urlString: String?
    
    public init(className: String, urlString: String? = nil) {
        self.className = className
        self.urlString = urlString
    }
}

public protocol RouterDelegate: class {
    func routeTargetWithPath(_ path: RouterProtocol.Path) -> RouteTarget?
}
复制代码

那么Router获取界面方法相似于下面的代码:spa

static public func viewControllerWithPath(_ path: RouterProtocol.Path) -> UIViewController? {
        guard let target = Router.delegate?.routeTargetWithPath(path),
            let routableClass = NSClassFromString(target.className) as? Routable.Type  else {
                return nil
        }
        
        if let urlString = target.urlString,
            let url = routableClass.handleURLString(urlString, routePath: path) {
            return SFSafariViewController(url: url)
        }
        
        return routableClass.viewControllerWith(routePath: path)
    }
复制代码

这里我加了个解析路由表中的url字段和在原来的协议RouterProtocol中添加了handleURLString方法,主要是为了当路由表中出现url字段时候,跳转至网页而不是界面,固然URL的处理方式应该是直接交给实现协议的类处理的。

同理,你还能够经过定义好比handleMessageWithPath的方法来实现简单的模块间通信,这里再也不赘述。

目前看来还不错,得益于枚举咱们有了可靠的模块间调用方式,也能比较好的适配Objective-C编写的模块。而咱们维护Router只须要添加枚举项和对应路由表映射。

这里还能够再优化下,能够将RouterProtocol和Router拆分,如果当前模块不须要调用其余模块的话,只须要引入并实现RouterProtocol让其余模块调用便可。

引入子模块以及Swift的坑

写完全部子模块了,咱们只须要在主项目中像下图这样引入子模块,或者使用Cocoapods等组件化方式引入子模块。

如今,你可能觉得已经结束了,你能够愉快的在项目中使用路由了。其实并无那么简单,比方说你在这时候像下面的代码利用Router获取界面其实是不可行的,Router会返回nil,找不到对应的界面。

let loginVC = Router.viewControllerWithPath(.user(.login)) 
复制代码

在这以前咱们都理所固然的认为NSClassFromString能够帮助咱们动态的建立相应的Class,事实上由于Swift与Objective-C不一样,程序启动时模块中的Class是没有被加载的。所以下面这段代码实际结果为空

NSClassFromString("UserModule.LoginViewModel") == nil
复制代码

因此咱们须要对模块的Class进行“注册”以后才能使用。简单来讲咱们须要使用一次这个Class,能够是建立它也能够只是获取它。比方说你能够经过简单的let _ = LoginViewModel.self,以后Router就能够反射对应的Class了。

这里能够在各个模块中定义一个loadClass的方法,而后在启动的时候调用每一个子模块的loadClass方法。或者,每一个子模块中用Objective-C建立一个类,在其+load()方法中调用(这个方法在静态库中须要在linker flags中添加force_load参数)。相似于下面这样的写法:

#import "UserModuleRegister.h"
#import <UserModule/UserModule-Swift.h>

@implementation UserModuleRegister

+ (void)load {
    [UserModule loadClass];
}

@end
复制代码

总结

到此为止,这个方案算是优缺点明显了,简单总结下吧。

优势:

  • 模块间耦合低。
  • 模块间的跳转是可靠的。
  • 能够适配兼容Objective-C编写的模块。

缺点:

  • 仍然没法彻底避免硬编码,你须要将枚举映射路由表中对应的key。
  • 模块间的通信不够灵活。
  • 你在维护路由的自己的同时还可能须要每一个子模块去维护它加载Class的方法。

对于我来讲,这个方案上最大的问题在于Swift中的Class不能像Objective-C同样在启动时加载,即便有解决方法也显得不够完美。

就像我一开始说的,没有最好的方案,只有最合适的方案。 这其中的利弊,仍是要交给项目交给团队来权衡。

Demo地址

若是你有其余的方式去优化这个方案的话,欢迎留言讨论。

相关文章
相关标签/搜索