Creating a decoupled Router and ViewFactory

Viewed 202

I am experimenting with routing in an iOS app.

My current approach is hit a road block however, I am creating a ViewControllerFactory and a NavigationControllerRouter.

The challenge I have is that the ViewControllerFactory needs to instruct the router on which route is required next and the NavigationControllerRouter requires the factory to pass a view controller to the navigation controller.

How can these 2 components communicate without creating a strong coupling?

enum Route {
  case login
  case home
}

protocol ViewControllerFactory {
  func create(for route: Route) -> UIViewController
}

class VCFactory: ViewControllerFactory {
  func create(for route: Route) -> UIViewController {
    switch route {
    case .login:
      let viewController = UIViewController()
      viewController.view.backgroundColor = .purple
      return viewController
    case .home:
      let viewController = UIViewController()
      viewController.view.backgroundColor = .red
      return viewController
    }
  }
}

class NavigationControllerRouter {
  private let navigationController: UINavigationController
  private let factory: ViewControllerFactory

  init(_ navigationController: UINavigationController, factory: ViewControllerFactory) {
    self.navigationController = navigationController
    self.factory = factory
  }

  func route(to route: Route) {
    let viewController = factory.create(for: route)
    navigationController.pushViewController(viewController, animated: true)
  }
}

I set this up as follows

@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {

  var window: UIWindow?

  private lazy var navController = UINavigationController()
  private lazy var router: NavigationControllerRouter = {
    let viewControllerFactory = VCFactory()
    return NavigationControllerRouter(navController, factory: viewControllerFactory)
  }()

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

  func configure(_ window: UIWindow) {
    self.window = window
    self.window?.makeKeyAndVisible()
    self.window?.rootViewController = navController

    router.route(to: .login)
  }
}

I had considered passing a weak reference for the router into the view controller factory, then injecting that into any rendered views, however this then couples the 2, especially if I decide to use the view factory for something like child views, which shouldn't know about the router at all.

1 Answers

I don't see anything wrong with passing a reference to your VCFactory, providing it is a weak ref to ensure of no retain cycles.

This is much the same pattern as used in the MVP approach where the view has a reference to the presenter and the presenter has a reference to the view.

You could ensure your ViewControllerFactory does not contain unused references to a router by perhaps using a protocol to extend it.

protocol Routable {
  var router: NavigationControllerRouter? { get }
}

...

class VCFactory: ViewControllerFactory, Routable {

  weak var router: NavigationControllerRouter?
....
}

You should then be able to pass a reference on creation

  private lazy var router: NavigationControllerRouter = {
    let viewControllerFactory = VCFactory()
    let router = NavigationControllerRouter(navController, factory: viewControllerFactory)

    viewControllerFactory.router = router

    return router
  }()
Related