Build complex apps with MVVM-C

Introduction

When it comes to app architecture patterns MVC (Model-View-Controller) has long been the pattern to go. The one that was used in tutorials by Apple and others. And in general there is nothing wrong with it. When your app stays within a reasonable scale MVC is a good pattern that does not create too much boiler plate code. But, and most of us will have been there already, as soon as the app reaches a certain size with this basic pattern view controllers get bigger and bigger and harder and harder to maintain. Since your view controllers will not only be responsible for handling updates of its view, it also handles all the business logic related to that view.

Even if you follow all clean codes paradigms and you do your best with naming your properties and methods as semantic as possible, which you always should. And even if you do your best with abstracting complexity where necessary. It will become a time intensive task for you and others to find your way around the code base. In addition to that, doing refactorings and adding new features will become more painful overtime since their side effects to other parts of the code become harder to predict.

So, what can you do to avoid these obstacles and create an app architecture that is not only easy to maintain.

What is MVVM?

MVVM stands for Model - View - View Model and describes the split between the model, the view and the view‘s view model. The view in that case represents the view controller and its corresponding view. Whereas the view model takes the role of the bridge between the view and the model. It handles the entire business logic that is related to the view. This means it handles not only providing the view with new data from the model but it also updates the model with data coming from the view, e.g. user inputs.

MVVM archtiecture

As you can see in the graphic above the view model has only one connection to its view. Even though, of course, there are quite a few approaches you can take to this pattern, I find, it keeps every thing nice and easy to understand if only the corresponding view (or rather view controller) knows its corresponding view model. The view provides its view model only with the objects it needs. That way you can keep presenting your view controllers as you are used to and it is always clear what models are used by this view controller and its view model.

The code snippets show a simple example of how an implementation can look like.

public class ViewController: UIViewController {
    private var viewModel: ViewModel
    @IBOutlet private weak var titleLabel: UILabel!
    
    public init(dependency: Dependency) {
        self.viewModel = ViewModel(dependency: dependency)
        super.init(nibName: String(describing: ViewController.self), bundle: nil)
    }
    
    @available (*, unavailable)
    public required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    public override func viewDidLoad() {
        super.viewDidLoad()
        self.bindToViewModel()
    }
    
    private func bindToViewModel() {
        self.titleLabel.text = self.viewModel.title
    }
}

public class ViewModel {
    public let title: String
		private let dependency: Dependency
    
    public init(dependency: Dependency) {
				self.dependency = dependency
        self.title = self.dependency.object.getTitle()
    }
}

How to exchange data between view and view model?

Since you can still present your view controllers one after the other like you do with MVC, let us have a look at how you can handle the data exchange between one view and its view model.

Again, there are quite a few ways of doing this, and I will only show one here. If you look at different examples and other tutorials, you will find different approaches to this. I can only say that I have used this approach in production and find it rather easy and simple to understand. Moreover, it is versatile enough to build features on top of it or to refactor parts if necessary.

public class ViewController: UIViewController {
    private var viewModel: ViewModel

    ...

		@IBAction private func buttonPressed(_ sender: UIButton) {
        self.viewModel.addButtonPressed()
    } 
}

public class ViewModel {
    ...

		private let dependency: Dependency
    
    public init(dependency: Dependency) {
				self.dependency = dependency

				...
    }

		public addButtonPressed() {
				self.dependency.addNewItem()
		}
}

As you can see in the code snippet above only the view controller holds a reference to its view model and provides it with all models that are used by the view model. Moreover, it calls its view model‘s properties to provide its view with data and calls its view model‘s methods to provide it with view updates.

And what is that C?

Even though, MVVM is already quite an improvement coming from MVC you can further split responsibilities and therefore concerns. C in that case stands for Coordinator. Coordinators play an important role in handling the presenting and dismissing of view controllers. They are called in between view controllers and are always used to present or dismiss a certain set of view controllers. That way no view controller is responsible to present another view controller anymore. This makes it easier to organize the view controller chain and since you further split up concerns it is easier to add view controllers later on.

MVVM-C architecture

The graphic above nicely shows how the coordinator further splits up concerns because only the coordinator holds a reference to the view controllers it coordinates and, as I l already mentioned, no view controller presents or dismisses other view controllers.

Here, is another point where you can interpret the pattern in different ways. You can either be very strict and let the coordinator coordinate every view controller the is presented. Including e.g. alert controllers or you can be a little bit less strict about this pattern and e.g. clearly define which view controller presentation or dismissal is handled by the coordinator and which is handled by the view controller it is presented on. Another option to add coordinators to your architecture would be to connect the coordinator to the view models instead of the view controllers. So, if you find e.g. tutorials that use this approach do not be surprised. Many roads lead to Rome, so to speak. The code snippet below shows an example of how an implementation to this can look like.

public class Coordinator: UIViewController, ViewControllerDelegate {
    private let dependency: Dependency

    public init(dependency: Dependency) {
        self.dependency = dependency

        super.init(nibName: nil, bundle: nil)
    }

    @available (*, unavailable)
    public required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    public override func viewDidLoad() {
        super.viewDidLoad()
        
        self.definesPresentationContext = true
        let viewController = ViewController(dependency: self.dependency)
        viewController.delegate = self
        
        self.present(viewController, animated: true)
    }

    // MARK: - ViewControllerDelegate

    public func showOther(_ viewController: ViewController) {
        let otherViewController = OtherViewController(dependency: Dependency)
				otherViewController.delegate = self

				self.present(otherViewController, animated: true)
    }

    // MARK: - OtherViewControllerDelegate

    public func close(_ viewController: OtherViewController) {
        self.dismiss(animated: true)
    }
}

public protocol ViewControllerDelegate: AnyObject {
		func showOther(_ viewController: ViewController)
}

public class ViewController: UIViewController {
		public weak var delegate: ViewControllerDelegate
    private var viewModel: ViewModel

    @IBOutlet private weak var titleLabel: UILabel!
    
    public init(dependency: Dependency) {
        self.viewModel = ViewModel(dependency: dependency)
        
        super.init(nibName: String(describing: ViewController.self), bundle: nil)
    }
    
    ...

		@IBAction private func otherButtonPressed(_ sender: UIButton) {
        self.delegate?.showOther(self)
    }
}

Final thoughts

After working with MVVM-C in production for a while now I can say that this pattern makes it fairly easy to get know the code base and to navigate around it. Moreover, it makes it easy to add features that also might affect a number of other view controllers because of the separation of concerns in terms of who handles what and who deals with what. Of course, it does not solve all problems and you should think about further improving your architecture by adding e.g. dependency injection to make providing view controllers and their view models with the necessary objects easier. In addition to that, you could also add a reactive framework like e.g. RxSwift to make view updates across multiple views easier and to reduce some boiler plate code that often makes view controllers rather complicated.

All in all, I would say that for projects of a certain size one should probably use "at least" MVVM to structure the code base. And if you are dealing with a number of view controllers, one should think about adding coordinators. Also if the project might grow in the future.

Where to go from here?

App Architecture - Objc.io

RxSwift Introduction Tutorial