👩‍💻 If you're just looking for code to copy, check out our public RxVIPER Xcode template


VIPER (View, Interactor, Presenter, Entity, and Router) has long been the go-to architecture for iOS developers everywhere. While we here at Linguistic love VIPER, we realize that the mobile landscape is changing, especially with the recent introduction of new iOS concepts such as Combine and SwiftUI (cue the Bob Dylan). iOS development is becoming more reactive, and as iOS paradigms change the architectures associated with them do as well.

To be honest, when we first heard the news, we panicked: “So Apple ripped off RxSwift? Does that mean Model-View-ViewModel (MVVM) is now the official architecture of iOS? Did we make a mistake choosing VIPER?”

However, we quickly realized that the concept of ViewModels, just like Views, Interactors, and Presenters are just that: concepts. They are all components that build an architecture, and do not inherently contain conflicting ideologies. After all, both MVVM and VIPER are designed to separate business logic from view logic, with the latter simply being slightly more verbose than the prior.

So we started to ask ourselves: if iOS is going all-in on reactive programming, how can we adopt our VIPER architecture to a reactive model without having to choose between MVVM and VIPER? Is it possible, and how do we continue to enforce the contracts set forth by each architecture?

After several rounds of research, we landed on a terrific article by Tibor Bödecs, outlining a VIPER approach using SwiftUI and Combine. Given how young SwiftUI still is, we deemed it best to hedge our bets with our current infrastructure of UIKit and RxSwift. The result was VVMMIPER, an architecture whose name doesn't really “roll off the tongue”. So let's call it RxVIPER: Reactive VIPER.

Here's what it looks like:

At first glance, this diagram looks strikingly similar to plain old VIPER, with the exception of two new elements: ViewModel and Module.

The ViewModel & Module

ViewModel

The core concept borrowed from MVVM is the VM: ViewModel. A view model is a data class (in this case a Swift struct) that contains RxSwift Relays and Subjects that act as the bridge between the Presenter and View (for information on Relays and Subjects classes, see here and here). In most cases, the Presenter  pushes data into the ViewModel and the View consumes it. The one exception to this rule, at least in our case, is the concept of UI “action triggers”.

Let's look at an example.

Below we have some code for Linguistic user profiles. All the view does is load the user profile picture into a UIImageView (Presenter → View) and triggers an image selection modal for changing the profile picture when you tap it (View → Presenter).

We start by looking at the ViewModel struct that will allow for communication between the View and the Presenter.

// ProfileViewModel.swift

import Foundation
import RxSwift 
import RxRelay

// MARK: - UI Actions

public enum ProfileAction {
  case .changeProfilePicture
}

// MARK: - ViewModel

public struct ProfileViewModel {
  let actionTrigger = PublishRelay<ProfileAction>()
  let profilePicture = BehaviorRelay<UIImage?>(value: nil)
}

We define all UI actions (such as tapping the profile picture) via the enum ProfileAction, then emit them via a PublishRelay. We store the profile picture as an optional UIImage  inside a BehaviorRelay, which has an initial value of nil.

Over in the View, we set up our Rx bindings to/from the ViewModel during our viewDidLoad method:

// ProfileView.swift

import Foundation
import UIKit
import RxSwift
import RxCocoa

final class ProfileView: UIViewController, ViewInterface {
  var viewModel: ProfileViewModel!
  var presenter: ProfilePresenter!
  
  private let disposeBag = DisposeBag()
  
  // ...UI setup code omitted...
  
  override func viewDidLoad() {
    super.viewDidLoad()

    // Map all taps of the UIImage to an action trigger
    // and bind it to the ViewModel
    self
      .profilePictureImageView
      .rx
      .tap
      .map { ProfileAction.changeProfilePicture }
      .bind(to: viewModel.actionTrigger)
      .disposed(by: disposeBag)
      
    // Bind the profile picture stored in the ViewModel
    // to the profile picture UIImageView
    self
      .viewModel
      .profilePicture
      .bind(to: profilePictureImageView.rx.image)
      .disposed(by: disposeBag)
      
    self.presenter.viewDidLoad()
  }
}

Notice that we call the viewDidLoad() method on the Presenter last. This is important because we need to ensure the View is bound to the ViewModel before the Presenter consumes or listens to any data from it. We can then use a little Rx magic to wire the View up to the ViewModel.

Now we have the remote profile picture bound to the UIImageView's image, and when the image view is tapped, an action trigger is published telling the Presenter to handle the action.


💡 A Quick Side Note

What else is great about view models in VIPER is now we can just pass the entire view model to any custom subviews and use RxSwift bindings to automatically update their UI when data changes from the Presenter:

let customView = MyCustomView(viewModel: self.viewModel)

Finally, let's take a look at how the Presenter handles all this:

// ProfilePresenter.swift

import Foundation
import RxSwift

final class ProfilePresenter: PresenterViewInterface {
  var viewModel: ProfileViewModel!
  var router: ProfileRouter! 
  var interactor: ProfileInteractor!
  
  private let disposeBag = DisposeBag()
  
  func viewDidLoad() {
    // Tell the Interactor to load the profile picture
    // from the backend and bind it to the ViewModel
    interactor
      .getCurrentProfilePicture()
      .filter { $0 != nil }
      .bind(viewModel.profilePicture)
      .disposed(by: disposeBag)
      
    // Listen to any incoming action triggers and delegate
    // the task to the either the Interactor or Router
    viewModel
      .actionTrigger
      .subscribe(
        onNext: { [weak self] action in
          switch action {
            // The user wants to change the profile picture,
            // so open the image selection modal from the Router
            case .changeProfilePicture:
              self?.router.openProfilePictureSelector(delegate: self)
          }
        }
      )
      .disposed(by: disposeBag)  
  }

We retrieve the current user's profile picture from the Interactor, filter out any nil values to prevent unneeded UI updates, then bind it to the ViewModel for the View to consume. Similarly, we subscribe to the action triggers fired by the View so we can handle them accordingly using internal methods, the Interactor, or the Router. In this case, our only UI action is the .changeProfilePicture enum value, so we use a basic switch statement to match the incoming action and open the image selector via the module's Router class.

Module

You might have noticed the force unwrapping (! operator) we're doing around all of our core VIPER components (ProfileViewModel!, ProfilePresenter!, etc.). This is due to the fact that while we aren't explicitly initializing these class members, they're still being initialized by the Module class.

Module classes basically extract out the logic that other VIPER implementations might place inside the Router: component assembly and dependency injection. Their .build() method is the entry point to the entire module.

Take a look at our ProfileModule:

// ProfileModule.swift

import Foundation
import UIKit 

final class ProfileModule: ModuleInterface {
    typealias View = ProfileView
    typealias Presenter = ProfilePresenter
    typealias Router = ProfileRouter
    typealias Interactor = ProfileInteractor
    typealias ViewModel = ProfileViewModel
    
    static func build(userID: String) -> UIViewController {
        let presenter = Presenter()
        let interactor = Interactor()
        let router = Router()
        let viewModel = ViewModel()
        let view = View()

        view.presenter = presenter
        view.viewModel = viewModel

        presenter.viewModel = viewModel

        router.view = view

        // Only the Interactor should know what data to load
        interactor.userID = userID

        self.assemble(
            presenter: presenter,
            router: router,
            interactor: interactor
        )

        return view
    }
}

And if we look at the parent protocol that handles our assemble method:

// VIPER.swift

public extension ModuleInterface {
    static func assemble(presenter: Presenter, router: Router, interactor: Interactor) {
        presenter.interactor = (interactor as! Self.Presenter.InteractorPresenter)
        presenter.router = (router as! Self.Presenter.RouterPresenter)

        interactor.presenter = (presenter as! Self.Interactor.PresenterInteractor)

        router.presenter = (presenter as! Self.Router.PresenterRouter)
    }
}

You can see we are basically handling dependency injection in a single method, assigning all needed class members before the module is even returned. Now, in order to create a fully reactive, operating profile view all we need to call from other modules is:

ProfileModule.build(userID: "1001")

Wrap Up

Now that you are familiar with the new components introduced in RxVIPER, the rest is fairly straightforward. Every bridge between module components that was previously handled by delegates can now be handled by RxSwift subscriptions. As you can see in the above examples, we subscribe to Interactor methods directly from the Presenter and handle their results at the site of subscription without the need for an Presenter+Interactor Delegate.

The largest challenge we currently face is the difficulty in creating cross-module subscriptions. For instance, if we wanted to subscribe to the delegate methods of a new module:

// ProfilePresenter.swift 

self.router
    .openProfilePictureSelector()
    .subscribe(
      onNext: { [weak self] response in
        // do something with the module response
      }
    )
    .disposed(by: self.disposeBag)

Given the fact delegates support multiple method signatures and can be used for virtually anything, adapting module-specific delegates to reactive subscriptions can be pretty ambiguous. As you may have noticed, in our early example we pass the Presenter as the module delegate when opening the profile picture selector:

self.router.openProfilePictureSelector(delegate: self)

Currently this is the only usage of delegates in our RxVIPER approach, and we usually rely on the Presenter to act as the sole delegate for other modules. We are still exploring ways in which we can make the above subscription-based model more sustainable and easier to implement.

We're incredibly excited for the future of iOS development, and we hope you find our reactive approach to VIPER useful in your own architecture. You can download our RxVIPER XCode template on Github today.


Immerse yourself with Linguistic

Linguistic is an iOS application that connects you with native speakers and language learners from all around the globe.

Download for free today.