Create AppRouter by Using Dependency Injection

Leo Wirasanto
5 min readJan 16, 2024

Background

Routing is one of the essential thing in iOS App development and other platform for sure. To handle the routing, we usually create a new class for each page that responsible to do the navigation thing. For example we have 2 page (HomeViewController & HistoryViewController), so we should have 2 router for each page as well (HomeRouter & HistoryRouter).

example by using VIPER

This is the most common thing we do to handle the routing.

Problem

However, in my opinion this way can be improved by using dependency injection so in the feature we don’t have to construct the destination page which take 4 or more LOC.

For example, we are using VIPER as the design pattern which contains (View, Interactor, Presenter, Entity, Router) and you need to construct 4 of them for everytime you create a new method to navigate to the particular page, in this case we are going to set HistoryPage as navigation destination.

func navigateToHistoryPage() {
let interactor = HistoryInteractor()
let router = HistoryRouter()
let presenter = HistoryPresenter(interactor: interactor, router: router, parameters: parameters)
let viewController = HistoryViewController(presenter: presenter)
router.viewController = viewController
interactor.presenter = presenter

navigationController?.pushViewController(viewController, animated: true)
}

Solution

Setting Up

  • First step, let’s create 3protocols and 1 enum like the code below.
protocol BaseModule: AnyObject {
func displayPage(parameters: [String: Any]?)
}

protocol Module {
var moduleKey: String { get }
}

protocol ProductRouter: AnyObject {
func presentModule(_ module: Module, parameters: [String: Any])
}

enum PageModule: Module {
case home
case history

var moduleKey: String {
switch self {
case .home:
return "home-page-key"
case .history:
return "history-page-key"
}
}
}

PageModule enum is like a list of available modules/pages.

Module is the interface for PageModule, it has moduleKey for filtering purpose.

BaseModule is the interface for every Modules (HomeModule, HistoryModule, etc).

ProductRouter is the main interface that responsible to presenting the registered Module. We’ll see who is implementing the ProductRouter and why it’s become the main here.

  • Second Step, create a protocol IAppRouter and implement ProductRouter that we’ve created at the first place. After that, create a new class AppRouter and implement the IAppRouter protocol.
protocol IAppRouter: ProductRouter {
var navigationController: UINavigationController? { get set }

func presentView(_ view: UIViewController?, animated: Bool)
func pushView(_ view: UIViewController?, animated: Bool)
func popVC(animated: Bool)
}

class AppRouter: IAppRouter {
private let productConstructor: (_ appRouter: IAppRouter) -> ProductRouter
private var rootVC: UIViewController?
var navigationController: UINavigationController?

init(productConstructor: @escaping (_ appRouter: IAppRouter) -> ProductRouter) {
self.productConstructor = productConstructor
}

// Implementation of IAppRouter - ProductRouter
func presentModule(_ module: Module, parameters: [String : Any]) {
let product = productConstructor(self)
product.presentModule(module, parameters: parameters)
}

func presentView(_ view: UIViewController?, animated: Bool) {
guard let view = view else { return }
view.modalPresentationStyle = .overFullScreen
navigationController?.present(view, animated: animated)
}

func pushView(_ view: UIViewController?, animated: Bool) {
guard let view = view else { return }
navigationController?.pushViewController(view, animated: animated)
}

func popVC(animated: Bool) {
navigationController?.popViewController(animated: animated)
}
}
  • Third Step, create a new class ModuleFactory and implement ProductRouter as well. This class responsible to invoke the BaseModule protocol which implemented by each modules (HomeModule & HistoryModule) whenever the ProductRouter protocol invoked.
class ModuleFactory: ProductRouter {
private var appRouter: IAppRouter

init(appRouter: IAppRouter) {
self.appRouter = appRouter
}

// store all modules
var modules: [String: BaseModule] {
var result: [String: BaseModule] = [:]

// constructs the module class (this example is using VIPER)
let homeModule = HomeModule(appRouter: self.appRouter)
let historyModule = HistoryModule(appRouter: self.appRouter)

result[PageModule.home.moduleKey] = homeModule
result[PageModule.history.moduleKey] = historyModule
return result
}

// from ProductRouter
func presentModule(_ module: Module, parameters: [String : Any]) {
// filter the registered modules by module key from parameter
if let module = modules[module.moduleKey] {
module.displayPage(parameters: parameters)
}
}

static func getAssemblies() -> [Assembly] {
var result: [Assembly] = [
LandingPageAssembly(),
HistoryPageAssembly()
]

return result
}
}

For the example of BaseModule implementation, let’s see this HistoryModule class

class HistoryModule: BaseModule {
let appRouter: IAppRouter

init(appRouter: IAppRouter) {
self.appRouter = appRouter
}

// displayPage method is the part of BaseModule implementation.
func displayPage(parameters: [String: Any]) {
let interactor = HistoryInteractor()
let router = HistoryRouter(appRouter: self.appRouter)
let presenter = HistoryPresenter(interactor: interactor, router: router, parameters: parameters)
let viewController = HistoryViewController(presenter: presenter)
router.viewController = viewController
interactor.presenter = presenter

// using method from IAppRouter protocol since it stores the parent navigationController
appRouter.pushView(viewController, animated: false)
}
}

We only construct the page/module here at Any_Page_Name_Module.swift class.

We also inject appRouter to the HistoryRouter class.

  • Last step, start the injection at SceneDelegate class.
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var appRouter: IAppRouter?
var navigationController: UINavigationController?

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: scene)

// Creating base page for window's root
let basePage = UIViewController()
basePage.view.backgroundColor = .white
let navigationController = UINavigationController(rootViewController: basePage)

// set the navigationController variable & construct the appRouter
self.navigationController = navigationController
self.appRouter = createAppRouter()

window.rootViewController = navigationController
window.makeKeyAndVisible()
self.window = window

// invoking the function to open selected module.
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
self?.open(PageModule.home)
}
}

func open(_ module: Module) {
// sample of usage: we only need 1 line of code
appRouter?.presentModule(module, parameters: [:])
}

func createAppRouter() -> IAppRouter? {
let productConstructors: (_ appRouter: IAppRouter) -> ProductRouter = { appRouter in
appRouter.navigationController = self.navigationController
return ModuleFactory(appRouter: appRouter)
}
let assembler = Assembler()
assembler.apply(assemblies: ModuleFactory.getAssemblies())
return AppRouter(assembler: assembler, productConstructor: productConstructors)
}

Implementation

I know, it’s a long journey, isn’t it? :)

Here we are! Now we can show the any registered page by using this 1 line of code.

In Any router class, let’s navigate to history page.

appRouter?.presentModule(PageModule.history, parameters: [:])

Closing Statement

This is not a perfect example to use the dependency injection into Routing mechanism, let’s use this for reference only.

You also can use Swinject to make it more effective.

Thank you for reading!

--

--