MVVM challenges

Michał Laskowski

May 19, 2021


There are many articles about MVVM. Most of them provide some snippets of code that may be useful. Somehow it happens, that more often than not they are not really helping when you are writing your production-ready code. And to be honest, after working with multiple teams and interviewing it is clear a lot of developers use MVVM differently. Some use Reactive Programming, some closures, some even delegates; and sometimes we are not even sure if the approach is even still an MVVM.

Common questions that pop up are:

We will take a look into two approaches:

  1. ViewModel is created and passed into view layer (UIView or UIViewController) from outside
  2. View (UIView or UIViewController) creates a view model for itself

Kickstarter approach

Let’s start with the latter. This is something we have seen in Kickstarter’s MVVM approach, and what our team followed for some time. Example here, a view model for a cell.

I chose a cell to illustrate an example where you need to pass some data into view model. This isn’t always needed for view controllers that can operate without passing any parameters, like previous screen’s state.

Let’s imagine we have a cell that needs to present a thumbnail, title and description. Seems pretty standard. We can imagine it as having a definition like this:

final class SimpleTableViewCell: UITableViewCell {
  @IBOutlet private weak var imageView: UIImageView!
  @IBOutlet private weak var title: UILabel!
  @IBOutlet private weak var description: UILabel!
  
  // here comes view model
  private(set) var viewModel = SimpleCellViewModel()
}

But how do we get the values for imageView, title and description? In Kickstarter approach a common thing is to create a configureWith(...) method, that passes any needed parameters into a view model.

Let’s imagine a SimpleViewModel definition, and assume we have some model of data:

import RxSwift

struct SimpleViewModel {
  func configureWith(model: Model) {
    modelProperty.onNext(model)
  }
  
  init(imageProvider: ImageProviding = ImageProvider()) {
    title = modelProperty.map { $0.title }
    description = modelProperty.map { $0.description }
    thumbnail = modelProperty.flatMap { 
        imageProvider.image(for: $0.thumbnailUrl)
    }
  }
  
  let title: Observable<String>
  let description: Observable<String>
  let thumbnail: Observable<UIImage>
  
  private let modelProperty = PublishSubject<Model>()
}

As you can see, all the outputs for view are reactive. Whether it is good or bad, it is up to discussion. What it means, when view model is observed from outside, is that all properties can change over time.

Overall, it is not that bad yet, imageProvider is injected in the constructor, with default implementation, and can be used directly in the constructor.
But we also saw implementations like this (please do not do it at home/work):

struct SimpleViewModel {
  func configureWith(model: Model, imageProvider: ImageProviding) {
    modelProperty.onNext(model)
    imageProviderProperty.onNext(imageProvider)
  }
  
  init() {
    title = modelProperty.map { $0.title }
    description = modelProperty.map { $0.description }
    
    thumbnail = modelProperty.withLatest(from: imageProviderProperty).flatMap { (model, imageProvider) in
        imageProvider.image(for: model.thumbnailUrl)
    }
  }
  
  let title: Observable<String>
  let description: Observable<String>
  let thumbnail: Observable<UIImage>
  
  private let modelProperty = PublishSubject<Model>()
  private let imageProviderProperty = PublishSubject<ImageProviding>()
}

You can easily imagine that with more parameters passed into view models, it becomes so much more complicated. There are different ways to handle that. One alternative is to use a tuple for all parameters - Kickstarter creates Properties for tuples that hold all parameters.

Worst case is storing all arguments in separate reactive properties, like above, and trying to zip or combineLatest on them. I have seen such implementations, and I wouldn’t say it looks or reads easily. Especially some time after implementation, when you need to modify it.

Another option could be using implicit unwrapped optionals for properties, and assigning values to them in configureWith method. But before we try to overcome issues that were created by creating configureWith, let’s imagine a different scenario.

Injected View Model - ‘standard approach’

Let’s imagine view model is just a reference, like views passed with IBOutlets, and is somehow injected from outside. Then cell definition would look like this:

final class SimpleTableViewCell: UITableViewCell {
  @IBOutlet private weak var imageView: UIImageView!
  @IBOutlet private weak var title: UILabel!
  @IBOutlet private weak var description: UILabel!
  
  // here comes view model
  var viewModel: SimpleCellViewModel! {
    didSet { 
    	guard let viewModel = self.viewModel else { return }
    	bindViewModel()
    }
  }
  
  override func prepareForReuse() {
  	super.prepareForReuse()
  	viewModel = nil
  }
  
  func bindViewModel() { ... }
}

We can start observing view model in different ways, and for a cell it makes the most sense to start observing it in didSet on the property, and stop observing in prepareForReuse.
The view model will need to be passed into cell, but this can easily be done in cell(atIndexPath:) when cell is dequeued.

As it often happens, when we are creating/dequeuing the cell, we have a model for it ready. Be it is provided by Core Data, or fetched from REST API, we have some of the data.

struct SimpleViewModel {
  init(model: Model, imageProvider: ImageProviding = ImageProvider()) {
    title = model.title
    description = model.description
    thumbnail = imageProvider.image(for: model.thumbnailUrl)
  }
  
  let title: String
  let description: String
  let thumbnail: Observable<UIImage>  
}

If you compare this approach with Kickstarter style, you will see we no longer need so many properties. We don’t need to store them because all the inputs can be specified in a constructor, and outputs (properties observed by a view) can be constructed from them.
We no longer need to store property for config, and imageProvider. And the only reactive element here is UIImage for thumbnail, because it is the only thing that is really asynchronous.
This means it is clear from outside/view level, what will change over time, and what will not.

And what we often saw in our projects, using Kickstarter style creates fully reactive view models. By refactoring them to a standard approach, we were able to reduce a number of reactive properties, simplify them, and in some cases even remove reactive code from them.

ViewModel can be injected in many ways into a view. It could be done with a segue, or a more complicated mechanism like Router/Flow Controller/Flow Coordinator could be used. More on that in a separate article.

Testing

As always, what is our code worth if we can’t test it. In Kickstarter approach, every view model’s output is reactive. This can be handled by using TestObserver.
This way all events that happen on output are recorded, and can be compared in tests after applying inputs to the view model.

But if not all outputs are reactive, it is much easier to test them. When some of them are, we can usually also focus on them, using very simple tools.

final class SimpleViewModelTests: XCTestCase {
	
	private var imageProvider: ImageProviderMock!
	
	override func setUp() {
		super.setUp()
		imageProvider = ImageProviderMock()
	}
	
	func testInitialState() {
		let model = Model(title: "title", description: "description", url: "")
		let viewModel = SimpleViewModel(model: model, imageProvider: imageProvider)
		XCTAssertEqual(viewModel.title, model.title)
		XCTAssertEqual(viewModel.description, model.description)
	}
	
	func testImageLoads() {
		let model = Model(title: "", description: "", url: "https://test.com/image.png")
		let viewModel = SimpleViewModel(model: model, imageProvider: imageProvider)
		XCTAssertEqual(imageProvider.requests.count, 0)
		
		imageProvider.response = SignalProducer.just(UIImage())
		
		var imageLoadedCount = 0
		viewModel.image.subscribe { _ in
			imageLoadedCount += 1
		}
		
		// test 1 request was made, and one output was provided
		XCTAssertEqual(imageProvider.requests, [model.url])
		XCTAssertEqual(imageLoadedCount, 1)
	}
}	

Testing it is a bit simpler when not everything is reactive. Even without using extra TestObservers, you should be able to write unit tests that make sure you do not make too many http requests or reload UI too often.

Sum up

If you are learning about MVVM right now, want to use it in a big project, or have been using Kickstarter approach so far, I highly recommend that you give standard constructors a try first. It can really simplify the implementation of view models, tests, and binding between views and view models.

Reactivity, be it ReactiveSwift, RxSwift, or Combine; is just a tool, that should not be overused. It is hard to understand what is truly asynchronous when everything is reactive, and code trying to glue properties together may become unreadable. Worse, it is becoming an unreadable boilerplate.

And if something starts to become asynchronous, it is not a big thing to adjust the implementation. We are developers, after all, we can modify the code as often as we want.

To be fair, there is a pro for keeping properties reactive - it decouples the property from the way it provides its value. The data source behind it could be synchronous or asynchronous, and a view should be ready for asynchronous scenario. But, remember YAGNI and KISS - most often a change from a standard property to a reactive one is simple.

Whatever approach you are using, ‘keeping it simple’ is the way. Your colleagues and your future self will thank you.