MVVMC – Adapting the MVVM Design Pattern at Runtastic

by Adam Studenic, Software Engineer iOS

Why we care about architecture

At Runtastic, we’ve already created 38 iOS apps, our team has grown to 20 iOS developers and our iOS codebase contains over 700,000 lines of code. A growing team leads to a growing codebase, which leads to higher complexity and more dependencies in code. This may end in disaster unless you care about architecture and follow some rules when creating software components:

  • Follow the single responsibility principle
  • Design for testability
  • Have clear dependencies
  • Keep your code readable and maintainable

We’ve all started with Apple’s MVC, which can turn into a Massive View Controller (been there, done that). Try running `find . -type f -exec wc -l {} + | sort -n` in the root of your project. You might find ViewControllers with thousands of lines of code, like we did.

Having realized that we were facing increasing challenges adhering to the above rules with MVC, we set out to look for other design patterns. Being a big topic nowadays within the iOS community, you can choose between a variety of design patterns including MVC, MVP, MVVM or VIPER. After some research, we decided to adopt MVVM, but some questions were left unanswered, such as routing (presenting a new screen) or data binding. Thus, we also took a closer look at VIPER, which has really good ideas on solving those questions. But with VIPER we’ve experienced the downside of there being quite a lot of boilerplate code as well as a steep learning curve for devs not used to this pattern. In the end, we incorporated our learnings from VIPER into our own adaption of the MVVM design pattern.

MVVMC – What’s the C?

The C stands for “calçots,” which is a specific type of onion of Catalan origin, same as our colleague and initiator of this pattern. Going through a series of screens in the app metaphorically means going from one onion layer to the next. The group of classes representing one screen will be called a “calçot” and is composed of the Model, View, ViewModel, Interactor and Coordinator.

Calçot composition

Let’s explain this diagram using a real use case that we have recently developed in our Runtastic app, such as groups of users. A user can be a member of multiple groups. You can see an overview of a user’s groups on the screen below.

Model

The Model is plain data that’s interpreted by a calçot (especially the ViewModel). It has no presentation logic or dependencies to other components.

struct GroupsList {
  // groups that are part of this model
  let groups: [Group]
  
  // category of a group (enum)
  let groupsCategory: GroupCategory
}

ViewModel

The ViewModel contains presentation logic for preparing data to be shown by the View (e.g. providing a localized groups category string). Any interactions it gets forwarded from the View are delegated to the Interactor or Coordinator, depending on the kind of interaction.

final class GroupsViewModel {
 
  private let interactor: GroupsInteractorProtocol
  private let coordinator: GroupsCoordinatorProtocol
 
  private var groupsList: GroupsList {
    return interactor.groupsList
  }
    
  init(interactor: GroupsInteractorProtocol, coordinator: GroupsCoordinatorProtocol) {
    self.interactor = interactor
    self.coordinator = coordinator
  }
 	
  var groupsCategory: String {
    return groupsList.groupsCategory.localizedString
  }
 	
  func fetchGroups() {
                // interaction to be handled within calçot
    interactor.fetchGroups()
  }
    
  func dismissGroup() {
    // interaction leading to a different screen or calçot
    coordinator.dismissGroup()
  }
}

The above example shows how the ViewModel prepares data (e.g. the localized string representing a category of groups) to be presented by the View. Also, it delegates the interaction either to the Interactor if it is to be handled within the calçot (e.g. affecting the model) or to the Coordinator if it results in a navigation to a different calçot.

View

The View (typically `UIKit` classes such as UIView or UIViewController) only cares about the presentation of some data, which it gets from its ViewModel. Once the data is updated (e.g. as seen below through setting a new ViewModel), it reacts by updating its own UI.

final class GroupsViewController: UIViewController {
  
  var viewModel: GroupsViewModel {
    didSet {
      updateUI()
    }		
  }
  	
  init(viewModel: GroupsViewModel) {
    self.viewModel = viewModel
         	super.init(nibName: nil, bundle: nil)
  }
  
  private func updateUI() {
    // update UI
  }
  
  // ...
}

Interactor

The Interactor holds the model and contains the business logic to update the model based on interactions or events. Additionally, it deals with external dependencies (outside of its calçot), such as database access, network calls or communication with other components.

protocol GroupsInteractorProtocol {
  var groupsList: GroupsList { get }
  func fetchGroups()
}

final class GroupsInteractor: GroupsInteractorProtocol {
  
  private(set) var groupsList: GroupsList
  private let dataProvider: GroupDataProvider
    
  // ...
  
  func fetchGroups() {
    // fetch data from a data provider
    dataProvider.fetchGroups() { (groups, error) in
      // Error handling? Naaah, this stuff always works :D
  
      // Update the model with new data
      self.groupsList = GroupsList(groups: groups, groupsCategory: .joined)
    }
  }
}

In this example, `GroupsInteractor` is responsible for fetching the user’s groups from the data provider (e.g. network or database) and creating a new instance of `GroupsList`.

Coordinator

The main responsibility of the Coordinator is to navigate between screens and create new calçots where needed. Navigation from one screen to another (usually also represented through a calçot) is provided through a simple interface. When navigating to a different calçot, the Coordinator is also initializing and preparing all the required objects for the new calçot (including ViewController, ViewModel, Interactor and the new Coordinator).

protocol GroupsCoordinatorProtocol: class {
  func present(group: Group)
  func dismissGroup()
}
  
final class GroupsCoordinator: GroupsCoordinatorProtocol {
  weak var navigationController: UINavigationController?
    
  func present(group: Group) {
    // Preparing the new calçot
    let groupCoordinator = GroupCoordinator(navigationController: navigationController)
    let groupInteractor = GroupInteractor(group: group)
    let groupViewModel = GroupViewModel(interactor: groupInteractor, coordinator: groupCoordinator)
    let groupViewController = GroupViewController(viewModel: groupViewModel)
  
    // Navigate to the new screen
    navigationController?.pushViewController(groupViewController, animated: true)
  }
  
  // ...
}

In our example, the Coordinator provides the option to navigate from the groups overview to the group detail screen. When triggered (e.g. user tapped on a group), it also initializes the new calçot.

When the Model has changed…

View depends on ViewModel and ViewModel depends on Model. When the Model changes, the View needs to be notified and updated accordingly. There are several approaches to achieving this by using

  • the Observer pattern to observe changes to the Model (e.g. ObserverSet)
  • the Delegate pattern to explicitly communicate a change
  • KVO-based libraries (e.g. Swift Bond)
  • the new language features of Swift 4 related to KVO
  • Functional Reactive Programming (FRP) libraries (e.g. ReactiveCocoa, RxSwift)

We prefer to use the Observer pattern when there’s multiple interested objects in listening to changes and the Delegate pattern when there’s a one-to-one relationship between View and ViewModel. This is how GroupInteractorProtocol look like using ObserverSet.

protocol GroupInteractorProtocol: class {
  init(group: Group)
  
  var group: Group { get }
  var groupMembersDidChange: ObserverSet<Void> { get }
  
  func add(user: User)
    func remove(user: User)
  }
  
  final class GroupsCoordinator: GroupsCoordinatorProtocol {
    
    func present(group: Group) {
      // …
  
      // observe changes on the group members and set the View’s new ViewModel
      interactor.groupMembersDidChange.add { [weak groupViewController, weak groupInteractor] in
        guard let interactor = groupInteractor else { return }
        groupViewController?.viewModel = GroupViewModel(interactor: interactor, coordinator: groupCoordinator)
      }
    	
    // ...
    }
  }
}

When the Model has changed, the Interactor reacts by creating a new ViewModel and setting it on View. In this example, we are using an immutable ViewModel, as we generally strive for immutable ViewModels. Certain scenarios (e.g. more specific communication for partial UI updates) might justify the use of mutable ViewModels, though potentially making them more complex and harder to test.

How MVVMC improves testability

A major benefit of using structural design patterns is that they increase the testability of the created software components. We follow a few basic guidelines that help us test the different parts of MVVMC. First of all, we generally use protocols instead of specific classes. This allows you to simply inject a mocked object, thus making it much easier to test the actual business logic inside a class as well as its interaction with other components. Let’s see what this could look like for the ViewModel.

class GroupsFakeInteractor: GroupsInteractorProtocol {
  // …
  
  var groupsList = GroupsList(groups: [], groupsCategory: .joined)
    
  func fetchGroups() {
    groupsList = GroupsList(groups: (0..<50).map { _ in Group(id: "0", name: "Test", members: []) }, groupsCategory: .joined)
    groupsListDidChange.notify()
  }
}

class GroupsFakeCoordinator: GroupsCoordinatorProtocol {
  var presented = false
  
  func present(group: Group) {
    presented = true
  }
  
  // ...
}

With the injected classes, we can now write an Integration Test to verify whether the ViewModel integrates correctly with both the Interactor and Coordinator.

final class GroupsViewModelTests: XCTestCase {
  
  private var interactor: GroupsFakeInteractor!
  private var coordinator: GroupsFakeCoordinator!
  private var viewModel: GroupsViewModel!
  
  override func setUp() {
    super.setUp()
        
    interactor = GroupsFakeInteractor()
    coordinator = GroupsFakeCoordinator()
    viewModel = GroupsViewModel(interactor: interactor, coordinator: coordinator)
  }
  
  override func tearDown() {
    // ...
    super.tearDown()
  }
  
  func testFetchUserGroups() {
    let exp = expectation(description: "Fetch groups")
        
    // This completion block should be called when groups are fetched
    interactor.groupsListDidChange.add { [weak interactor] in
      XCTAssertEqual(interactor?.groupsList.groups.count, 50)
      exp.fulfill()
    }
        
    // Fetch mocked user groups
    viewModel.fetchGroups()
        
    waitForExpectations(timeout: 2.0, handler: nil)
  }
}

Conclusion

MVVMC is our approach to answering some open questions about MVVM and applying a common design pattern to our code. It is still evolving, so we would be happy to get some feedback and are especially curious to see how you are applying it to solve some of the architectural challenges you are dealing with in your apps. We have also created an example GitHub repository, in which you can find all of the code from this article.

***

Tech Team We are made up of all the tech departments at Runtastic like iOS, Android, Backend, Infrastructure, DataEngineering, etc. We’re eager to tell you how we work and what we have learned along the way. View all posts by Tech Team