RxVIP: Clean Reactive Architecture

Getting a team to decide on an architecture is no easy task. The same questions tend to come up when creating a new application:

  • How do you choose the right architecture?
  • Is there a right architecture?
  • Are we all just getting on the bandwagon of the most popular architecture?
  • Do we decide to challenge ourselves by using the least popular and most confusing architecture because we are Code Ninjas 🥋 and Programming Warriors 🤺... I mean... That's how they get popular right? RIGHT?
  • How many emojis are too many? 🤔

Before continuing any further, I should mention that this post talks in depth about Reactive programming, specifically RxSwift. If you are unfamiliar with RxSwift, I would suggest you take a look at some great resources here:

Already know about RxSwift? Well then let's get started!


Table of Contents


Decisions, decisions...

The right way to make decisions in any team is great communication. The iOS team at Trōv meets regularly (at least 3 times a week) where we are encouraged to bring any questions or concerns to the table. The team is always open to feedback, and discussion is very active.

We started thinking about a new architecture pattern to replace our MVC (or Massive View Controller) approach. In a small project, the size of a typical ViewController might not get out of hand, but the larger the project gets, the bigger the challenge.

I won't compare existing architectures, there are plenty of blogs that have done just that. Instead, I will talk about our team's decisions regarding architecture, and our experimentation and adoption of reactive programming (Rx).


Enter VIP

We were introduced to the space faring Uncle Bob 🚀, we loved his guidelines for clean coding, and from there, we found Clean Swift.

While the team was happy with the Clean Swift guidelines, we found that it was not working perfectly for us.

We also wanted to start using reactive programming, and this VIP structure wasn't set up for Rx, so we devised our own:


Rx + VIP(R) = RxVIP(R)

This is a diagram of what our architecture looks like:

RxVIP Diagram

As you can see, the ViewController owns an InteractorInput, a Presenter, and a Router.

The InteractorInput is a protocol, which is implemented by the Interactor. The Interactor also implements a protocol called InteractorOutput which is owned by the Presenter.

In a typical VIP structure, the [View -> Interactor -> Presenter -> View] cycle is one-directional. In the Rx structure, since we need to subscribe to the presenter, we modified the cycle so that the view owns both an Interactor and a Presenter, and the Presenter has an unowned reference to the Interactor, via the InteractorOutput protocol.

Also, data flows through reactive calls in a cyclical style:

  • View Controller sends action events to Interactor and subscribes to Presenter events
  • Interactor sends data to Presenter
  • Presenter notifies observers to present necessary data


VIP(R) Scene

First, let's define our VIP(R) architecture Scene:

  • View Controller
    • The UIViewController sends inputs to the Interactor and handles outputs from the Presenter.
  • Interactor
    • The Interactor receives input from the View Controller, mostly via action events. The Interactor does work, and converts data into an InteractorResponse object, forwarding it to the Presenter.
  • Presenter
    • The Presenter receives input from the Interactor, interprets the data, and forwards the output as a ViewModel back to the View Controller.
  • Router
    • The Router is in charge of routing to different scenes.

We mentioned an InteractorResponse object sent by the Interactor to the Presenter, and a ViewModel object that is sent by the Presenter back to the View Controller. These objects are model objects devised to remove any obstruction from the receiver's handling logic.

  • The Presenter receives the InteractorResponse and knows how to convert it into a ViewModel.
  • A view controller receives the ViewModel object, and knows how to display all parameters contained within.

We also need an object to configure all of these interactive components together. We call it the Configurator.

Finally, to privatize function calls, and enable the Interactor inputs and outputs to be siloed for cleanliness, the InteractorBoundary was created. The InteractorBoundary isn't an object, it's our Interactor paradigm.

Let's go through each of these pieces in detail.


View(Controller)

The view (but since we are in iOS, the view is represented by a UIViewController) is used to being big and bloated, but how can we make it lean?

Let's go through an example. Let's take a scenario that most apps have: A login screen.

First let's define a view controller for a login screen.

import UIKit

final class LoginViewController: UIViewController {

}

Great! Now, we need to make sure that our LoginViewController has the appropriate VIP(R) capabilities: An Interactor, a Presenter, and a Router.

import UIKit

final class LoginViewController: UIViewController {  
    var interactor: LoginInteractorInput!
    var presenter: LoginPresenter!
    var router: LoginRouter!
}

Notice how the Interactor is defined as an InteractorInput, this is where our separation of concerns comes into play. Let's leave our LoginViewController like this for now, and let's start building these objects.


Interactor

We could build an Interactor with inputs and outputs visible to all parties, but it felt a little invasive. The solution was to create the InteractorBoundary. Let's define one for the Login screen:

import RxSwift

struct LoginInteractorResponse {  
    var user: User?
}

protocol LoginInteractorInput: class {  
    func login(withEmail email: String, password: String)
}

protocol LoginInteractorOutput: class {  
    var model: Observable<LoginInteractorResponse> { get }
}

Hey look! Rx is in the building!

Remember, the InteractorBoundary is not an object, it is a paradigm for creating an Interactor. The boundary needs an input, and output, and a response object. As you can see in our example, the input has a trigger for when it's time to login and the output returns a response object. In this case, our Response object contains an optional User. For the sake of this example, we will assume a User object is already created.

Let's define our LoginInteractor:

import RxSwift

struct LoginData {  
    let email: String
    let password: String
}

final class LoginInteractor {  
    fileprivate let loginSubject = PublishSubject<LoginData>()
}

extension LoginInteractor: LoginInteractorInput {  
    func login(withEmail email: String, password: String) {
        let loginData = LoginData(email: email, password: password)
        loginSubject.onNext(loginData)
    }
}

extension LoginInteractor: LoginInteractorOutput {  
    var model: Observable<LoginInteractorResponse> {
        return loginSubject
            .flatMap { LoginUseCase().login(withEmail: $0.email, password: $0.password) }
            .map(LoginInteractorResponse.init)
    }
}

As you can see, our LoginInteractor adheres to both the LoginInteractorInput and LoginInteractorOutput protocols. These are nicely separated, and now when an object needs to communicate with the Interactor, it can only send data in one direction.

Notice our response observable. We flatMap the loginSubject where we are taking advantage of a LoginUseCase. Use cases are our "worker" objects. In this case, the user wants to login. Let's assume that the UseCase object does the necessary work to log the user in, and the login function that we are calling here returns the necessary user data. We then map that data to a LoginInteractorResponse. Note that the response object will need a custom initializer to initialize itself with the user data retrieved from the use case.

Back to the View Controller

Now that we have the interactor set up with a login trigger, let's add that code to the LoginViewController.

import UIKit

final class LoginViewController: UIViewController {  
    var interactor: LoginInteractorInput!
    var presenter: LoginPresenter!
    var router: LoginRouter!

    @IBOutlet weak var emailField: UITextField!
    @IBOutlet weak var passwordField: UITextField!

    @IBAction func didTapLoginButton() {
        let email = emailField.text ?? ""
        let password = passwordField.text ?? ""
        interactor.login(withEmail: email, password: password)
    }
}

I added two text fields to my view, and a login button. When the user taps on the login button, the IBAction will trigger the interactor's login function. So far so good, but what about reacting to the event? Let's set up our presenter.


Presenter

The Presenter is in charge of receiving data from the Interactor and sending it back to the ViewController. Since we separated the Interactor into an input and output, the Presenter only cares for the Interactor's output:

import RxCocoa  
import RxSwift

final class LoginPresenter {  
    unowned var interactor: LoginInteractorOutput

    var viewModel: Driver<LoginViewModel> {
        return interactor.model
            .map { [unowned self] model in
                return self.translate(model)
            }
            .asDriver(onErrorJustReturn: LoginViewModel())
    }

    init(_ interactor: LoginInteractorOutput) {
        self.interactor = interactor
    }

    private func translate(_ model: LoginInteractorResponse) -> LoginViewModel {
        return LoginViewModel()
    }
}

Notice that our LoginInteractorOutput is defined as an unowned var. This allows us to have our cyclical dependency without worrying about reference cycles.

Our viewModel is an RxCocoa trait called a Driver. The LoginViewModel is the view model object we talked about earlier. This object is the model object that the view controller needs to visually present any data. Let's assume for now that the LoginViewModel has relevant information for the view controller. In the case of your project, this could be String that could be mapped to UILabel, Bool that could be mapped to UIControl.isEnabled properties, etc.

We also create a translation function, so that we can translate the LoginInteractorResponse into a LoginViewModel

Back to the View Controller

Now that we have the presenter set up with a the presentation ViewModel, we should subscribe to the presenter

presenter.viewModel.drive(onNext: { viewModel in  
    // present some view model things!
    // Here are some examples:
    self.loginButton.isEnabled = viewModel.isLoginButtonEnabled
    self.emailField.layer.borderColor = viewModel.emailFieldBorderColor
}).disposed(by: disposeBag)

Let's add that code to the LoginViewController

import RxCocoa  
import RxSwift  
import UIKit

final class LoginViewController: UIViewController {  
    var interactor: LoginInteractorInput!
    var presenter: LoginPresenter!
    var router: LoginRouter!

    @IBOutlet weak var emailField: UITextField!
    @IBOutlet weak var passwordField: UITextField!
    @IBOutlet weak var loginButton: UIButton!

    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        configurePresenter()
    }

    private func configurePresenter() {
        presenter.viewModel.drive(onNext: { viewModel in
            // present some view model things!
            // Here are some examples:
            self.loginButton.isEnabled = viewModel.isLoginButtonEnabled
            self.emailField.layer.borderColor = viewModel.emailFieldBorderColor
        }).disposed(by: disposeBag)
    }

    @IBAction func didTapLoginButton() {
        let email = emailField.text ?? ""
        let password = passwordField.text ?? ""
        interactor.login(withEmail: email, password: password)
    }
}

I added an outlet to the login button, and I set up the presenter.

Now when the user taps the login button, the following sequence of events is triggered:

  • The Interactor is told to login
  • The Interactor does any login work via the LoginUseCase
  • The Interactor formats any login data into a LoginInteractorResponse
  • The Interactor sends the Presenter the LoginInteractorResponse
  • The Presenter translates the LoginInteractorResponse into a LoginViewModel
  • The Presenter drives the LoginViewModel to the LoginViewController
  • Since the LoginViewController is subscribed to the Presenter, it gets notified to drive the view model.

Phew! That's a lot of work being done. This may seem like a lot, but it reduces code, and increases developer intent.


Router

The Router is a fairly simple object. At it's most basic, it can be defined like so:

final class LoginRouter {  
    unowned var viewController: LoginViewController

    init(_ viewController: LoginViewController) {
        self.viewController = viewController
    }
}

The router is owned by a view controller, but it also needs a reference to said view controller, so we make that reference weak. This will allow the LoginRouter to decide what Scene should be displayed next, and then use the view controller to present the next scene's view controller.


Connecting it all together

Last step! Our LoginViewController had references to an Interactor, a Presenter, and a Router, but they were never initialized! That's a job for a LoginConfigurator:

final class LoginConfigurator {  
    func configure(_ viewController: LoginViewController) {
        let router = LoginRouter(viewController)
        let interactor = LoginInteractor()
        let presenter = LoginPresenter(interactor)

        viewController.interactor = interactor
        viewController.presenter = presenter
        viewController.router = router
    }
}

The configurator creates the necessary objects and assigns them appropriately. But when is it called? Let's add a function to do this in our LoginViewController:

private func configure(configurator: LoginConfigurator = LoginConfigurator()) {  
    configurator.configure(self)
}

override func awakeFromNib() {  
    super.awakeFromNib()
    configure()
}

There, now we can add that to our LoginViewController

import RxCocoa  
import RxSwift  
import UIKit

final class LoginViewController: UIViewController {  
    var interactor: LoginInteractorInput!
    var presenter: LoginPresenter!
    var router: LoginRouter!

    @IBOutlet weak var emailField: UITextField!
    @IBOutlet weak var passwordField: UITextField!
    @IBOutlet weak var loginButton: UIButton!

    private let disposeBag = DisposeBag()

   override func awakeFromNib() {
        super.awakeFromNib()
        configure()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        configurePresenter()
    }

    private func configure(configurator: LoginConfigurator = LoginConfigurator()) {
        configurator.configure(self)
    }

    private func configurePresenter() {
        presenter.viewModel.drive(onNext { viewModel in
            // present some view model things!
            // Here are some examples:
            self.loginButton.isEnabled = viewModel.isLoginButtonEnabled
            self.emailField.layer.borderColor = viewModel.emailFieldBorderColor
        }).disposed(by: disposeBag)
    }

    @IBAction func didTapLoginButton() {
        let email = emailField.text ?? ""
        let password = passwordField.text ?? ""
        interactor.login(withEmail: email, password: password)
    }
}


Final Thoughts

At the time of this post, we have started implementing this new architecture over the last several weeks. So far, it's increased code cleanliness, and it's reduced clutter and confusion for a team of developers to work on.

There are still some kinks that I am sure we will work through. This article was written based on our third or fourth iteration of the architecture, and I am sure we will continue to tweak it as it suits our needs.

At this point, our RxVIP(R) structure has the following advantages:

  • Clean code
  • Reduced file sizes, and lines of code
  • More testable, easier to inject mock pieces
  • Easy to adhere to SOLID principles

With some disadvantages:

  • Harder to understand at first glance
  • Larger number of files
  • More complex architecture than MVC

For our team, the advantages definitely outweighed the disadvantages, and we couldn't be happier with out decision.