Back to the basics of MVVM on iOS

I won’t explain in detail where MVVM comes from or what the difference is between MVC etc. There are plenty of good articles out there about this. Instead, we want to focus on what solution MVVM brings and how we can make it work the right way.

Firstly, let’s be clear that Model-View-ViewModel is an UI architectural pattern, not a software architecture per se. It is often seen as an architecture in iOS development because the UI management generally represents 3/4 of the work on iOS with most of the computing coming from external sources, like an API.

This simple misconception leads to cascading problems as developers will try to fit elements that MVVM doesn’t support and try to make their own truth out of the pattern.

Developers I have worked with often focus on « what is a model? », « what are view models? » and where is the separation. For example, as a developer we will often wonder « where does the network request sits here? in the model or the view model? ».

For all we know, the Model layer is a blackbox where “business logic” happens. This means pretty much anything which is not UI and presentation, so your network layer should logically fit there, although, there is no clear way of organising the inside of the Model layer in MVVM. You will have to use a different architectural pattern here to manage your data, databases, API requests, background computing and so forth.


Part 1: layers

That said, let’s first define each layer:

Models

The part we are interested in here is data. Any data organised in any way! It can come from an external API or a local database like Realm. The way we get the data doesn’t matter.

A simple model could be:

struct User {
  let firstname: String
  let surname: String
}

Views

Any UI elements, like UIButton, UIView or even UIViewController. Yes, the UIViewController too, because they handle a lot of UI components naturally, navigation bar, view, window etc. It’s what we get for trying to apply MVVM to an environment thought out for MVC.

If you’re not including UIViewController into Views, one will end up with an extra layer that wouldn’t fit into MVVM.

A simple view could be:

class ProfileView: UIView {
	let titleLabel = UILabel()
	
	var viewModel: ProfileViewModel? {
		didSet {
			titleLabel.text = viewModel?.title ?? ""
		}
	}
	
	init() {
		super.init(frame: .zero)
		
		addSubview(titleLabel)
		
		// Auto Layout
		NSLayoutConstraint.activate([
			titleLabel.leftAnchor.constraint(equalTo: leftAnchor),
			titleLabel.rightAnchor.constraint(equalTo: rightAnchor),
			titleLabel.topAnchor.constraint(equalTo: topAnchor),
			titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor)
		])
	}
	
	required init?(coder: NSCoder) {
		fatalError("init(coder:) has not been implemented")
	}
}

ViewModels

This is our special place. A made-up layer for the purpose of populating Views from the content of Models. ViewModels are indeed, adapter design pattern elements. They adapt the non-displayable format information contained in models to displayable format information rendered in Views.

A simple ViewModel could be:

class ProfileViewModel {
	let title: String
	
	init(user: User) {
		title = "\(user.firstname) \(user.lastname)"
	}
}

A few important notes:

  • The View layer never know anything about the Model layer.

  • The ViewModel layer does not hold any reference to the View layer.

  • The View layer holds a direct reference to the ViewModel layer.

As you might have noticed, we do not have any data binding in place between the View and its View-Model. We do this on purpose to make sure we understand the basics first.


PART 2: DATABINDING

An important part of MVVM is databinding. It is how we have an “automated” update of the UI (View) following the state of data (ViewModel) which happens when data are modified (Model).

Whilst the data update can happen through different means: API request, user input and various computation, we will want to notify the ViewModel layer from those updates in a discretionary way as opposed to automated. This is to avoid updating the screen several times while data is changed one by one. It is part for the business logic to know when sets of data are complete. It could be that you need to do multiple requests to fetch all the information needed on a single screen.

Once the ViewModels are notified, updating the screen should be automated. The idea is that once MVVM is set, the screen will update at the will of business logic, and the business logic should not have to worry about updating the UI.

In swift, the easiest way to do a databinding is to combine a variable and a closure. Although the capabilities are limited, it makes it easier to understand what databinding is.

Now our stack could look something like this:

// Model
struct User {
	let firstname: String
	let lastname: String
}

// ViewModel
class ProfileViewModel {
	// Databinding
	var title: String?
	var onTitleUpdate: (() -> Void)?
	
	var user: User? { // By assigning new data, the viewmodel is notified by the model layer that it needs to update the UI
		didSet {
			guard let user = user else {
				title = ""
				onTitleUpdate?()
				return
			}
			title = "\(user.firstname) \(user.lastname)"
			onTitleUpdate?()
		}
	}
}

// View
class ProfileView: UIView {
	let titleLabel = UILabel()
	
	let viewModel: ProfileViewModel // The view holds a reference to its viewmodel
	
	init(viewModel: ProfileViewModel) {
		self.viewModel = viewModel
		
		super.init(frame: .zero)
		
		titleLabel.text = viewModel.title
		addSubview(titleLabel)
		
		// Auto Layout
		NSLayoutConstraint.activate([
			titleLabel.leftAnchor.constraint(equalTo: leftAnchor),
			titleLabel.rightAnchor.constraint(equalTo: rightAnchor),
			titleLabel.topAnchor.constraint(equalTo: topAnchor),
			titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor)
		])
		
		// Binding
		viewModel.onTitleUpdate = { [unowned self] in
			self.titleLabel.text = self.viewModel.title
		}
	}
	
	required init?(coder: NSCoder) {
		fatalError("init(coder:) has not been implemented")
	}
}

That’s easy to understand. When we update the Title, we trigger our closure so that the View, which is bound to its ViewModel. Now we can also forecast how such an implementation would create a lot of boilerplate.

There are a lot of databinding systems out there ready to use. For the purpose of this article and the sake of understanding the foundations, we will design a minimal binding system ourselves:

// Generic databinder variable
class DynamicVariable {
	typealias Binding = (T) -> Void
	
	var value: T 
		 
			
		
	
	
	private var binder: Binding?
	
	init(_ value: T) {
		self.value = value
	}
	
	func bindAndFire(_ binder: @escaping Binding) {
		self.binder = binder
		binder(value)
	}
}

Using our new databinder our stack could now look like this:

// Model
struct User {
	let firstname: String
	let lastname: String
}

// ViewModel
class ProfileViewModel {
	// Databinding
	var title = DynamicVariable(nil)
	
	var user: User? { // By assigning new data, the viewmodel is notified by the model layer that it needs to update the UI
		didSet {
			guard let user = user else {
				title.value = nil
				return
			}
			title.value = "\(user.firstname) \(user.lastname)"
		}
	}
}

// View
class ProfileView: UIView {
	let titleLabel = UILabel()
	
	let viewModel: ProfileViewModel // The view holds a reference to its viewmodel
	
	init(viewModel: ProfileViewModel) {
		self.viewModel = viewModel
		
		super.init(frame: .zero)
		
		addSubview(titleLabel)
		
		// Auto Layout
		NSLayoutConstraint.activate([
			titleLabel.leftAnchor.constraint(equalTo: leftAnchor),
			titleLabel.rightAnchor.constraint(equalTo: rightAnchor),
			titleLabel.topAnchor.constraint(equalTo: topAnchor),
			titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor)
		])
		
		// Binding
		viewModel.title.bindAndFire({ [unowned self] title in
			self.titleLabel.text = title
		})
	}
	
	required init?(coder: NSCoder) {
		fatalError("init(coder:) has not been implemented")
	}
}

Step 3: Complete it

You might still wonder what to put in your Model layer. Short answer: It doesn’t matter in the scope of MVVM.

There are plenty of architectural patterns to complete MVVM. Commonly your Model layer will be divided into 2 x sub-layers: entities (data models) and services. Services could be singletons, or use a dependency injection. Yet, services are not a part of MVVM. It’s important to avoid mixing everything.

On your ViewModel layer, you could abstract your ViewModels with protocols in order to make your View testable by inserting dummy ViewModels containing mocked models.

You could use heavy reactive libraries like RxSwift to bind everything together. The “downside” is that it comes with its own set of patterns to be efficient. It might also be confusing for junior developers with a poor understanding of basic MVVM so some training could be beneficial.

MVVM is an excellent architectural pattern in the sense that it makes the separation of concerns obvious. This makes it easier for junior developers to avoid making mistakes and allows senior developers to enforce tests against those younger developers mistakes.

Extra ressources