How to implement Coordinator pattern with RxSwift

After weeks experimenting different patterns and code structures, I wanted to go further in functional reactive programming and see how to take advantage of it while following Coordinator pattern. This post describes how integrate RxSwift with Coordinator pattern and which mistakes to avoid.

Prerequisites

Before diving in the code, you need to be a bit familiar with Coordinator pattern or have some experience with RxSwift.

As a short reminder, Coordinator patterns aims to handle navigation logic, the actual user flow and remove this layer of complexity from any ViewController / ViewModel.

On the other side, RxSwift is a functional reactive programming framework. It ables you to create stream of changes and react to them as observers.

Depending of your code project, it might actually be easier to implement RxSwift into your Coordinator pattern than having a RxSwift codebase and integrate Coordinator pattern.

Today, I’ll cover the first one, using a Coordinator pattern with a Router example.

Coordinator

The first part is to make our Coordinator become more reactive, meaning to get feedback when a coordinator is started or ended.

import RxSwift

protocol Coordinator : class {
    var childCoordinators : [Coordinator] { get set }
    func start() -> Observable<Void>
}

One of the challenge with Coordinator pattern is to know when to free child coordinators. RxSwift becomes really handy to get this feedback via an event when a coordinator is going to be deallocated.

class BaseCoordinator : Coordinator {
    var childCoordinators : [Coordinator] = []

    func start() -> Observable<Void> {
        fatalError("Start method should be implemented in inherited class.")
        return .never()
    }

    func coordinate(_ coordinator: Coordinator) -> Observable<Void> {
        self.store(coordinator: coordinator)
        return coordinator
            .start()
            .do(onNext: { [weak self, weak coordinator] _ in
                self?.free(coordinator: coordinator)
            })
    }

The BaseCoordinator returns an observable when started which should be completed when the attached view is removed from the UI layer. Therefore, we can free the coordinator at the same time.

Note that [weak self, weak coordinator] is necessary to avoid creating another strong reference, we already have one from childCoordinators.

Router

In my previous blog post, I used a Router element to dissociate Navigation events from View layer. It also allows a callback when a view is removed from the stack. That avoid me to re-implement a back button to force deallocating the coordinator.

We need to adapt this router to also be more reactive.

import RxSwift

extension Reactive where Base: Router {

    func push(_ drawable: Drawable, isAnimated: Bool) -> Observable<Void> {
        return Observable.create({ [weak base] observer -> Disposable in
            guard let base = base else {
                observer.onCompleted()
                return Disposables.create()
            }

            // push Router as usual
            base.push(drawable, isAnimated: isAnimated, onNavigateBack: {
                observer.onNext(())
                observer.onCompleted()
            })

            return Disposables.create()
        })
    }
}

Implementation

Both Router and BaseCoordinatorare ready to be used. We only need to adapt the previous code to our new one, starting from AppDelegate.

Here, we have our first entry point, AppCoordinator. We need to subscribe to its start to make sure it’s going to cascade each event.

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    private var appCoordinator : AppCoordinator!
    private let disposeBag = DisposeBag()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        window = UIWindow()
        self.appCoordinator = AppCoordinator(window: window!)
        appCoordinator
            .start()
            .subscribe()
            .disposed(by: disposeBag)

        return true
    }

Wait, where is the Window visibility?

I kept this one into the AppCoordinator. I prefer the element that build the first view keeps responsibility to first display.

Here is how it looks in AppCoordinator.

class AppCoordinator : BaseCoordinator {

    let window : UIWindow
    let disposeBag = DisposeBag()

    init(window: UIWindow) {
        self.window = window
        super.init()
    }

    override func start() -> Observable<Void> {

        let navigationController = UINavigationController()
        let router = Router(navigationController: navigationController)
        let myCoordinator = MyCoordinator(router: router)

        window.rootViewController = navigationController
        window.makeKeyAndVisible()

        return self.coordinate(myCoordinator)
    }

Here, we prepare our Router with a UINavigationController that we also keep for the UIWindow. Since myCoordinator is going to be the first display, the observable returned self.coordinate(...) is also served to end this flow.

In short, when MyCoordinator flow is finished, AppCoordinator will automatically be finished too. Both will be deallocated together.

Finally, last piece is MyCoordinator, the very first one building a view.

class MyCoordinator : BaseCoordinator {

    let router: Router
    let disposeBag = DisposeBag()

    init(router: Router) {
        self.router = router
    }

    override func start() -> Observable<Void> {

        let viewModel = MyViewModel()
        let viewController = MyViewController()
        viewController.viewModel = viewModel

        viewModel.didSelect
            .subscribe(onNext: { [weak self] _ in
                // trigger next coordinator
            })
            .disposed(by: disposeBag)

        return router.rx.push(viewController, isAnimated: true)
    }

    // ...
}

This time, we use the Router as trigger to deallocate this flow. When the matching view controller isn’t part of the stack anymore, it will complete the observable which will also deallocate MyCoordinator.


In conclusion, we’ve seen how to migrate our codebase to a reactive one with RxSwift while keeping our Coordinator pattern in place. As I mentioned earlier, the big challenge is to keep in mind which element to keep reference and which one should free other elements.

Happy coding

© 2023 Benoit Pasquier. All Rights Reserved
Author's picture

Benoit Pasquier

Software Engineer πŸ‡«πŸ‡·, writing about career development, mobile engineering and self-improvement

ShopBack πŸ’°

Singapore πŸ‡ΈπŸ‡¬