Swift Routing Component (I) The purpose and implementation idea of using routing

This is my original, reproduced please indicate the source juejin.cn/post/703221…

Choice of scheme

What does a route really need to do?

  1. Map to a controller by key
  2. Instantiate a controller
  3. Resolve the parameter transfer
  4. Page jump

Here’s how the code implements each logic.

Map to a controller by key

There are two methods:

  1. By maintaining a PLIST file and adding routing tables at development time.
  2. At runtime, through business registration, each business registers the key to the route, maintaining a routing table in memory.

Let’s start with 1. This is also easy to do. The plIST is defined as follows:

So when you run it, you can read in this plist, and then you can do the map.

2. The simple way to do this is to create an entry function, and then each page to register the code to add to the entry function. Such as:

// Start with initialization
func onLaunch(a) {
	XXRouter.register(to: "native://to_home_page", routerClass: HomeViewController.self)
        XXRouter.register(to: "native://to_buy_page", routerClass: BuyViewController.self)
 	XXRouter.register(to: "http", routerClass: WebViewController.self)
 	XXRouter.register(to: "https", routerClass: WebViewController.self)}Copy the code

Both methods require an entry point, similar to centralized storage. Centralized storage also has advantages, convenient maintenance, intuitive, a glance to know how much definition, bad place is not convenient to do componentization. And componentization is something that people really need.

So the requirement here is to be able to decentralize the definition.

First I used scenario 2, and here I came up with a clever way to define a total XXRouter class and then let each business extend the XXRouter method itself. Then, at run time, you know how many routes are defined by iterating through how many methods a class has and then dynamically calling those methods to get the return value.

As follows, define an extension to XXRouter.

// Add a routing table
extension XXRouter {
    @objc func router_WebController(a) -> XXRouterModel {
        return XXRouterModel(to: "http", routerClass: WebController.self)}@objc func router_WebController_https(a) -> XXRouterModel {
        return XXRouterModel(to: "https", routerClass: WebController.self)}}Copy the code

The requirements of this multi-defined method are as follows:

  1. Must be @objc, runtime dependent.

  2. The function name must start with Router_, and you can define any name after it. You are advised to use the class name of the route. For example, the route class WebController can define the function router_WebController

  3. The input parameter of the required function is empty.

  4. The return value of this function is an XXRouterModel, which has the following values: 1) to: defines the route name, such as: native://course, or HTTP unique key, and performs hierarchical naming. 2) routerClass: Route target class, such as testClassName.self

Runtime traversal and dynamic invocation are as follows:

private static func configRoutersFromMethodList(a)- > [String: XXRouterModel] {
        var routerList: [String: XXRouterModel] = [:]
        var methodCount: UInt32 = 0
        let methodList = class_copyMethodList(XXRouter.self.&methodCount)
         
        if let methodList = methodList, methodCount > 0 {
            for i in 0..<Int(methodCount) {
                let selName = sel_getName(method_getName(methodList[i]))
                if let methodName = String(cString: selName, encoding: .utf8),
                   methodName.hasPrefix("router_") {
                    let selector: Selector = NSSelectorFromString(methodName)
                    if XXRouter.shared.responds(to: selector) {
                        if let result = XXRouter.shared.perform(selector).takeUnretainedValue() as? XXRouterModel {
                            routerList[result.to] = result
                        }
                    }
                }
            }
        }
        free(methodList)
        return routerList
    }
Copy the code
  1. Iterate over all the methods of a class
  2. Take out the methods that start with Router_.
  3. Dynamically call the method starting with Router_, retrieve its return value, and save it as a routing table.

Instantiate a Controller

At the routing level, there are two ways to do this, either automatic route instantiation. Or let the page that implements the routing instantiate itself.

For example, the route is automatically instantiated by receiving an AnyClass and then instantiating it with NSObject init() :

private static func getInstance(_ cls: AnyClass) -> XXRoutable? {
        if let instance = (cls as? NSObject.Type)?.init(), let routable = (instance as? XXRoutable) {
            return routable
        }
        return nil
    }
Copy the code

For example, let each page instantiate itself and then pass it on to the routing layer.

// Implement the routing protocol to support route hops.
extension WebController: XXRoutable {
    // Returns an instance of the routing protocol
    static func createInstance(params: [String : Any]) -> XXRoutable {
        let vc = WebController()
        vc.url = params["to"] as? String ?? "" // Routing parameters
        vc.value = params["value"] as? String ?? "" // Routing parameters
        return vc
    }
}
Copy the code

I chose the latter one. It makes sense to let the business instantiate itself and then give it to the routing layer. Including the fact that he can process the parameters and return them.

Resolve the parameter transfer

You can get all the parameters done through the above scheme.

Page jump

This is even easier. You just get the navigationController and push it. For convenience, I’ve written a global get navigationController to do the push. Look at the code

	/// Current navigation controller
    public static func currentNavigationController(a) -> UINavigationController? {
        return currentController().navigationController
    }
    
	public static func currentController(a) -> UIViewController {
        if let root = delegate().window??.rootViewController {
            return getCurrent(controller: root)
        } else {
            print("Abnormal problem, rootVC should not be called yet")
            assert(false."Abnormal problem, rootVC should not be called yet")
            return UIViewController()}}// get the currently displayed UIViewController recursively
    public static func getCurrent(controller: UIViewController) -> UIViewController {
        if controller is UINavigationController {
            let naviController = controller as! UINavigationController
            return getCurrent(controller: naviController.viewControllers.last!)}else if controller is UITabBarController {
            let tabbarController = controller as! UITabBarController
            return getCurrent(controller: tabbarController.selectedViewController!)}else if controller.presentedViewController ! = nil {
            return getCurrent(controller: controller.presentedViewController!)}else {
            return controller
        }
    }
Copy the code

With the above code, you can get a global navigationController like this

RouterUtil.currentNavigationController()
Copy the code

So push is easy.

RouterUtil.currentNavigationController()?.pushViewController(controller, animated: animated)
Copy the code

The specific implementation

Defining routing Protocols

By defining a routing protocol, all pages that implement the routing protocol can be redirected. Define an XXRoutable protocol as follows:

/// The routing protocol must be AnyObject. Other protocols such as struct and enum cannot implement this protocol
public protocol XXRoutable: AnyObject {
    The receiver is responsible for parsing its own parameters and returning a route instance
    static func createInstance(params: [String: Any]) -> XXRoutable

    /// Route logic processing
    func executeRouter(params: [String: Any].navRootVC: UIViewController?).
}
Copy the code
  1. To implement the routing page, return an instance, implement the createInstance method to return.
  2. The specific jump logic for the route to perform is implemented by implementing the executeRouter method. Alternatively, the routing component does the default route jump.

The default jump is as follows:

/// Default implementation of the routing protocol
public extension XXRoutable {
    /// Default route jump
    func executeRouter(params: [String: Any] =[:].navRootVC: UIViewController? = nil) {
        guard let controller = self as? UIViewController else {
            assert(false."Default route jump, need a Routable to inherit UIViewController")
            return
        }
        
        defaultPush(to: controller, params: params, navRootVC: navRootVC)
    }
}
// private methods in the route
public extension XXRoutable {
    /// native://my? userId=1&token=jdfsakbfjkafbf
    ///
    /// - Parameters:
    /// - controller: switches to the VC
    /// -params: additional parameters
    /// - navRootVC sometimes does not need to fetch currentVC
    func defaultPush(to controller: UIViewController.params: [String: Any] =[:].navRootVC: UIViewController? = nil) {
        let animated = (params["animated"] as? Bool) ?? true

        if navRootVC?.navigationController ! = nil {
            navRootVC?.navigationController?.pushViewController(controller, animated: animated)
        } else {
            RouterUtil.currentNavigationController()?.pushViewController(controller, animated: animated)
        }

    }
}
Copy the code

Register routing table

The extension XXRouter method is used to add routing tables, and the extension method returns a routing table model. The model is defined as follows

public class XXRouterModel: NSObject {
    // route name. You can optionally define a unique key
    /// native://course/detail, such a rule represents a local page.
    /// or HTTP, or HTTPS, which means the URL web page, directly responds with the WebView.
    public var to: String = ""
    public var routerClass: AnyClass = XXRouterModel.self // Route target class
    
    // The routing table definition of the routing module
    /// - Parameters:
    /// -t: specifies the route name
    /// - routerClass: route target class
    public convenience init(to: String.routerClass: AnyClass) {
        self.init(a)self.to = to
        self.routerClass = routerClass
    }
}
Copy the code

check

Definition of routes

Through the above, let a page to achieve routing, then only need to achieve the routing protocol, according to the routing protocol to achieve the corresponding method can have the function of routing. Add an extension method to add a routing table. If the overall definition is less, there are two methods, as follows:

// Normal route definition
extension TestClassName: Routable {
    static func createInstance(params: [String : Any]) -> Routable {
        return TestClassName()}}// Define an extension method
// Add a routing table
extension Router {
    @objc func router_TestClassName(a) -> RouterModel {
        return RouterModel(to: "native://testTest", routerClass: TestClassName.self)}}Copy the code

Use of routes

XXRouter.pushTo(jumpParams: ["to": "native://testTest"."name": "1"])
Copy the code

Automatic calibration

Look at the definition above, very simple, just two methods. However, the definition of both methods is required. So what if the user doesn’t follow the rules.

  1. For example, a class that implements the Routable protocol will fail to route if it forgets to add a routing table to the Extension router_xxx method.
  2. For example, adding a router_xxx method adds a routing table, but forgetting to implement the Routable protocol.
  3. For example, a router_xxx method is added to add a routing table, but the router_xxx method is added improperly. As a result, the method does not take effect. (@objc, function naming, function entry parameter, function return value, etc.)
  4. For example, if the key of the added routing table is duplicate, what should I do?

To address these omissions, I wrote a function that checks automatically. During route initialization, check for all of the above problems, interrupt if there are any problems, and then control the function to run only in Debug, does not affect the line. The function is as follows:

#if DEBUG
    /// Automatically check whether all route Settings comply with the specification. If any route Settings do not comply with the specification, the system interrupts directly
    private static func checkRoutableClassesSettingIsConform(a) {
        guard !isCheck else { return } // Check only once
        let expectedClassCount = objc_getClassList(nil.0)
        let allClasses = UnsafeMutablePointer<AnyClass>.allocate(capacity: Int(expectedClassCount))
        let autoreleasingAllClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(allClasses)
        let actualClassCount: Int32 = objc_getClassList(autoreleasingAllClasses, expectedClassCount)

        for i in 0 ..< actualClassCount {
            let currentClass: AnyClass = allClasses[Int(i)]
            if (class_getInstanceMethod(currentClass, NSSelectorFromString("methodSignatureForSelector:")) ! = nil),
               (class_getInstanceMethod(currentClass, NSSelectorFromString("doesNotRecognizeSelector:")) ! = nil),
               let cls = currentClass as? XXRoutable.Type {
                var isSet = checkList["\(cls)"]
                if isSet = = nil {
                    var curCls: AnyClass = cls as AnyClass
                    // If a parent adds a routing table, it means that it is OK, because the routing side does not allow subclasses to implement routing protocols. Subclasses can only inherit, override, or switch classes to implement routing
                    while let superCls = curCls.superclass() {
                        if checkList["\(superCls)"] ! = nil {
                            isSet = true
                            break
                        }
                        curCls = superCls
                    }
                }
                assert(isSet ! = nil."\(cls)The Routable protocol is implemented, but the routing table is not added, or the routing table configuration is not specified. Please check:\(cls).")
                checkList["\(cls)"] = true}}for (key, value) in checkList where value = = false {
            assert(false."\(key)Add routing table, but do not implement Routable protocol.\(key).")
        }
        isCheck = true
    }
#endif
Copy the code

The function does two things

  1. Routing tables are added, but Routable is not implemented
  2. The Routable protocol is implemented, but the routing table is not added, or the routing table configuration is not defined by the specification.

There is also a missing routing table duplicate. This can be solved by adding one more judgment to the routing table,

See the source code: XXRouter

Hope point Star support.