Photo under CC by Pascal
To start off we are going to consider every feature added to our application encapsulated within a module as a dynamic framework. This allows us to add to our application using these frameworks as building blocks.
Among many benefits, isolating code into features (modules) creates a scalable architecture that retains reliability over time.
However, its impossible to keep theses modules as isolated as depicted above. Consider the example of a ‘Home Screen’ module that is the landing page of an app and a ‘Login’ module that allows the user to log into the application. Its common that when a user tries to access some restricted part of a ‘Home’ feature a ‘Login’ page is launched. Likewise, the ‘Home’ feature probably wants to know whether the login action succeeded or not in order to show a ‘guest’ or logged in user experience.
Example of Login module wanting to present the Home feature and the Home wanting to present Login and get the result of the login action.
To enable module communication while maintaining loose coupling a module router is utilized to that accepts URL requests and routes requests between modules. This could mean asking our payment module to process a transaction or using the router to present the login module as in the example above.
Example of how the router works to communicate between modules.
This module router lives in a foundational framework that is shared across our application. Since this foundation is shared between every module we need to be very careful to limit what we put in it to avoid the coupling we are trying to prevent.
Since our Module Router is going to be universally accessible within the application we are going to make it a public singleton within our foundational framework.
public var SharedModuleRouter: ModuleRouter { struct Singleton { static let instance = InternalModuleRouter() } return Singleton.instance }
Now that modules can access the module router they will utilize three parts of it to perform inter-feature communication. To demonstrate these I’ll walk through its protocol:
1. Registering feature modules for the URLs they can consume:
public protocol ModuleRouter { // Register consumers for URLs func registerModuleClass(consumer: AnyClass, URLPath: String) …
2. Obtaining the common public interface for a module registered for a ModuleURL:
... func moduleForURLPath(URLPath: String) -> ModuleViewController? ...
3. Utilizing a HostApp object to perform the actual routing in the application level space:
func registerHostApp(hostApp: ModuleURLObserver) func hostApp() -> ModuleURLObserver? }
I’m not going to talk about how the HostApp object works here, but you can check out that aspect in the full example. Before we move onto discussing the module-level interface I’ll mention that we define our ModuleURLs through an enumeration and use a property on that URL to provide the path string:
public enum ModuleURL: Printable { case LoginModuleURL } extension ModuleURL: Path { public var path: String { switch self { case .LoginModuleURL: return “/login" } } }
The Module Interface
Conformance to the ModuleURLObserver protocol is required for the HostApp and ModuleViewControllers to handle routing requests.
public protocol ModuleURLObserver: class { func handleModuleURL(URLString: ModuleURL, successCallback: ModuleURL?, failureCallback: ModuleURL?) -> Bool }
We strictly refrain from adding any other methods to the ModuleViewControllers aside from this conformance to maintain uniformity and prevent the desire to add convience methods that couple features together.
Modules can issue requests to launch another feature through our shared router either directly or by using the host app:
// Directly issuing a request let loginURL = ModuleURL.LoginModuleURL if let loginViewController = ModuleRouter.viewControllerForURLPath(homeURL.path) { loginViewController.handleModuleURL(homeURL) homeViewController.navController?.pushViewController(loginViewController, animated: true) }
// Using the host app SharedModuleRouter.hostApp().handleModuleURL(ModuleURL.HomeModuleURL)
Modules handle these ModuleURLs by modifying their feature state to perform the expected behavior. For example, presenting the login page and kicking off any necessary network calls.
public override func handleModuleURL(URLString: ModuleURL, successCallback: ModuleURL? = nil, failureCallback: ModuleURL? = nil) -> Bool { switch URLString { case .Login: // present login page return true default: println("Invalid URL \(URLString)") return false } }
That’s it! Using this technique we can communicate between features while keeping them independent. For a more complex, working example of Modular Architecture take a look at the GitHub Example.