Recently I was doing a route design based on Swift. In fact, I felt that Swift enumeration was very suitable for routing when I first got to know Swift. For example, something like this could be written:

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

let loginVC = Router.viewControllerWithPath(.user(.login))
Copy the code

The nested design of enumerations, the ability to take arguments, and the ability to specify whether arguments are optional, makes inter-module calls more reliable. So how do you do that in today’s scenario of multi-player collaboration and componentized projects?

Of course, before this I still that sentence, there is no best plan, only the most appropriate plan.

Router and componentization

The problem with componentization is dependencies between components. For example, interfaces rely on routers to jump to, and routers rely on components to create interfaces, resulting in circular references between modules. Therefore, the Router cannot rely on submodules, we need to use NSClassFromString to create objects.

Of course we need to use the protocol to constrain the Class and use the Class to handle the corresponding routing operations. The protocol is similar to the following code, where the protocol design can be easily adapted to modules written in Objective-C.

public protocol Routable: NSObject {
    static func viewControllerWith(routePath: RouterProtocol.Path) -> UIViewController?
}
Copy the code

Routing Table Design

Thanks to Swift’s powerful enumeration, we can name our routing paths like this

public struct RouterProtocol {

    public enum Path {
    
        public enum User {
            case login
            case profile(userID: String)
        }
        
        case user(User)
        case search
    }
}
Copy the code

Next, we need a routing table file for our Router. Here I use JSON for convenience. We can choose other files according to the actual project and personal habits.

{
    "login": {"class":"UserModule.LoginViewModel"
    },
    "userProfile": {"class":"UserModule.UserViewModel"
    },
    "search": {"class":"SearchModule.SearchViewModel"}}Copy the code

Routing Table Parsing

Now we can write out the parse based on our enumeration and the corresponding field of the routing table.

You can actually delegate the resolution of the routing table to the main project, using proxies, to further reduce coupling.

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?
}
Copy the code

The Router method for retrieving the interface looks like this:

    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)
    }
Copy the code

I added a method to parse the URL field in the routing table and a handleURLString method to the original Protocol RouterProtocol. The main purpose of this method is to redirect the url field in the routing table to the web page instead of the interface. Of course, the URL handling should be handled directly by the class that implements the protocol.

In the same way, you can implement simple inter-module communication by defining methods such as handleMessageWithPath, which will not be described here.

So far, so good. Thanks to enumerations, we have a reliable way to call between modules, and it works well with modules written in Objective-C. We only need to add enumerations and routing table mappings to maintain the Router.

If the current module does not need to call other modules, it can simply import and implement the RouterProtocol for other modules to call.

Introducing submodules and pits for Swift

Once you’ve written all the submodules, just import them in the main project as shown below, or use componentization like Cocoapods to import them.

Now, you might think you’re done, and you can happily use routing in your project. It’s not that simple, for example, if you use the Router to get the interface like this it’s actually not feasible, the Router will return nil, you can’t find the interface.

let loginVC = Router.viewControllerWithPath(.user(.login)) 
Copy the code

Up until now we’ve taken it for granted that NSClassFromString will help us create classes on the fly, but in fact because Swift is different from Objective-C, classes in modules are not loaded when the program starts. So the following code is actually null

NSClassFromString("UserModule.LoginViewModel") == nil
Copy the code

So we need to “register” the module’s Class before we can use it. Simply put, we need to use the Class once, either to create it or just to get it. For example, you can simply let _ = loginViewModel.self and then the Router can reflect the corresponding Class.

You can define a loadClass method for each module and then call the loadClass method for each submodule at startup. Alternatively, each submodule creates a class in Objective-C and calls it in its +load() method (which requires the force_load argument to be added to Linker flags in the static library). Something like this:

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

@implementation UserModuleRegister

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

@end
Copy the code

conclusion

So far, the advantages and disadvantages of this plan are obvious. Let’s summarize briefly.

Advantages:

  • Low coupling between modules.
  • The jump between modules is reliable.
  • Can be adapted to compatible Objective-C written modules.

Disadvantages:

  • Still, hard coding can’t be completely avoided; you need to map enumerations to the corresponding keys in the routing table.
  • Communication between modules is not flexible enough.
  • You may need each submodule to maintain its class-loading method as well as the route itself.

For me, the biggest problem with this scenario is that classes in Swift don’t load at startup like Objective-C does, and even if there is a workaround, it’s not perfect.

As I said at the beginning, there is no best solution, only the most appropriate solution. The pros and cons should be weighed by the project and the team.

The Demo address

If you have other ways to improve this solution, feel free to leave a comment.