Remind iOS: MVVM for Dummies

Today we’re going to go over a design pattern that I struggled to understand initially and try to simplify it - the illustrious Model-View-ViewModel (MVVM) pattern. Let’s start with some background and motivation.

Success!*

I’m sure you’ve all been just dying to know how our Realm migration has shaken out over the last few months. (Right?) Our first release that was 100% Realm went out at the beginning of June. By all accounts, it was a big success. A migration from Core Data to Realm for an application our size was definitely a risk for us. Our timing was good though - Realm had just released their fine-grained notifications feature that we needed, and the school year was winding down, so our daily usage was falling off for the summer. This mitigated some of the risk of affecting our whole user base with an unforeseen migration issue. Since we released, our crash rate hasn’t gone up, we’ve continued to improve, and we all love working with Realm instead of Core Data, so I’m calling it a win. But it’s not all roses.

As I described in my talk about the migration, one of the advantages of Realm over Core Data is that when something goes wrong, it’s very clear what you’ve done wrong. You don’t have to deal with myriad random Core Data error messages to decipher with no real path pointing to the root problem. There is one particular class of error that permeates our codebase, and while Realm has made it very clear what that issue is, it hasn’t been easy to overcome. I consider us to still be in a transition period - we’ve obviously shipped our app with Realm, but we’re still in the process of updating our patterns to address the preexisting crashes that have been revealed by our migration.

Object has been deleted or invalidated

The deletion models between Core Data and Realm are very different. If you’re retaining a Core Data model that gets deleted, in most cases you’ll just find that all of that model’s properties have become nil. It won’t crash [usually] if you try to access a property; you may just get unexpected results in your UI, or you might crash if you try to put a nil property into an array or dictionary. Protecting array and dictionary assignments against nil values is very common in our codebase for all kinds of data, not just database models, so that was our basic strategy for avoiding Core Data crashes in many places.

NSDictionary *data = @{
	"id" : model.remoteId ?: @0,
	"name" : model.name ?: @""
}

In retrospect this is a pretty bad code smell that we should have addressed a long time ago. Now that we’re using Realm, the “protected” code above will crash if model has been deleted with an exception: Object has been deleted or invalidated.

We have a multi-client application where each client is interconnected via Pusher events. If you have our iOS app open and you delete one of your classes from our website, we delete that class from our Realm database, and you’ll see it disappear from your list. If you’re viewing a list of students in your class and one of your students leaves the class, you’ll see them disappear from the list. This is a pretty great user experience, but it also introduces an element of unpredictability throughout the application - at any point, the data populating the screen you’re looking at can be deleted from under you.

This unpredictability is what’s plaguing our application right now. We’ve made a habit of passing our database model objects around, retaining them in multiple classes, and accessing their properties on-demand whenever they’re needed. If that model gets deleted, there’s an unpredictable number of places that could be retaining that model, waiting to access its properties, and crash our app. It’s an unfortunate architectural decision that Past-Alex thought was a good idea, Present-Alex thinks is atrocious, and Future-Alex is going to have to fix.

MVVM to the Rescue

You’ve almost certainly read about the Model-View-Controller (MVC) pattern - it’s basically the foundation of Apple’s recommended application architecture. MVVM has a similar prefix, so it may seem like an alternative to or replacement for MVC, but that’s not the case. I don’t want to get into the weeds describing MVVM in too much detail - there are plenty of more academic sources to explore to get the basic concepts - I’m going to stick with what it means to our team and what we’ve cherry-picked from the pattern to suit our particular needs.

The interesting part of MVVM is the “VM” - the ViewModel. Thankfully, this is also the really simple part. I spent a lot of time confused about the whole pattern and how it helped us and how we could use it, until a very simple realization clicked for me:

A ViewModel is just an [immutable] object where you store the properties of a model that you actually care about for displaying your view.

Instead of passing around models directly, you just dump the things you care about into a ViewModel and pass that along instead. It’s really that simple.

By Example

As an example, let’s look at this screen of our application, which shows you a list of the people in your class:

image

We have a discrete set of information required for displaying this screen: the class’s primary key (for querying), its name, and how many members it has. In our previous architecture, we would initialize this view controller by passing in the Group entity and retaining it. On load we would set the screen’s title to the class’s name. Every time we need to query or filter for the members of the class, we would access its remoteId. Whenever we need to display the second section header, we would access its memberCount.

class PeopleViewController: UIViewController {
	
	let group: Group

	init(group: Group) {
		self.group = group
		super.init(nibName: nil, bundle: nil)
	}

	override func viewDidLoad() {
		super.viewDidLoad()

		self.title = self.group.name
		self.setupTableView()
	}

	private func setupTableView(query: String? = nil) {
		let groupMembers = GroupMember.objectsWhere("groupId == %@", self.group.remoteId)
		...
	}

	func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
		let header = ...
		header.memberCountLabel.text = "\(self.group.memberCount)"
		return header
	}
}

As previously described, if this class gets deleted somehow and we try to access one of its properties, we’re going to crash. That’s bad.

The root problem in all of this is mutability - our group can change unpredictably from underneath us. One thing people love about Swift is its affinity for immutable objects, so let’s adopt that for our MVVM setup. Instead of passing in the actual object, let’s extract the things we care about into a ViewModel and use that instead.

struct PeopleGroupViewModel {
	let remoteId: Int
	let name: String
	let memberCount: Int

	init(group: Group) {
		self.remoteId = group.remoteId
		self.name = group.name
		self.memberCount = group.memberCount
	}
}

class PeopleViewController: UIViewController {
	
	let viewModel: PeopleGroupViewModel

	init(viewModel: PeopleGroupViewModel) {
		self.viewModel = viewModel
		super.init(nibName: nil, bundle: nil)
	}
}

(It’s worth noting that we’re assuming that group has been freshly queried from the Realm, so there’s no chance it’s invalidated. When you apply this pattern throughout your codebase, you’ll effectively be passing Realm models around by primary key, which is a lot safer than passing objects around.)

Honestly, this is all MVVM means to our team. Stop retaining Realm models. Use immutable ViewModels. Pass objects by ID. This has the immediate benefit of not allowing object deletions to crash our application because we’re never referencing potentially invalidated models.

Into the Weeds (Implementation Details and Live Updating)

This section might get a little code heavy, but I thought it would be useful to share the implementation details we’ve built up to support this pattern throughout our app.

The most immediate question you might have from the above setup is, “How do you update the UI when something does change?” Previously, when we were retaining the model itself, we might elect to add KVO to the properties we want to monitor - the name and the memberCount - so we can update the UI in real time as we get Pusher events. We might also listen directly to Realm for write notifications and perform our updates then. In our MVVM setup, we’re going to generate and apply a new ViewModel whenever Realm notifies us that our group was updated through some lovely Swift magic.

Swift is a Protocol-Oriented Language, so let’s look at the couple of protocols we’ve deployed to solve this in the general case.

// "I can be instantiated with a Realm object of type 'Model'."

protocol RealmObjectViewModel {
    associatedtype Model: RLMObject
    
    init(realmObject: Model)
}
// "I would like to be notified when there's an updated ViewModel available
//  and when the entity I care about is deleted."

protocol RealmObjectViewModelConsumer: class {
    associatedtype ViewModel: RealmObjectViewModel

    func consumeNewViewModel(model: ViewModel)
    func monitoredRealmModelWasInvalidated()
}

That’s actually all we need in place in order to make a generic factory class that will listen for updates to a given Realm model and feed immutable ViewModels to a Consumer whenever there’s an update.

class RealmObjectPresentableFactory<Consumer: RealmObjectViewModelConsumer> {
    
    typealias Object = Consumer.ViewModel.Model
    typealias ViewModel = Consumer.ViewModel
    
    private var monitoredResults: RLMResults?
    private var changeToken: RLMNotificationToken?

    weak var consumer: Consumer?

    init(realm: RLMRealm, primaryKeyValue: CVarArgType) {
        
        if let primaryKey = Object.primaryKey() {
            self.monitoredResults = Object.objectsInRealm(realm, where: "SELF.%K == %@", args: getVaList([ primaryKey, primaryKeyValue ]))
            self.registerForUpdates()
        }
    }

    ...
}

Through the magic of associated types, all we have to provide to our factory is what kind of Consumer it’s going to have. From there we know what kind of ViewModel the consumer is expecting, and what kind of Model the ViewModel expects to be initialized with.

We set up an RLMResults so that we can receive updates to the object we care about it. Since we’re using a primary key, we know that our results will have at most 1 object.

From there it’s just some simple logic to respond appropriately to Realm’s change notifications.

private func handleRealmResultsChange(results: RLMResults?, change: RLMCollectionChange?, error: NSError?) {
    guard let change = change else {
        self.updateViewModel()
        return
    }
    
    if (change.insertions.count + change.modifications.count) > 0 {
        self.updateViewModel()
    }
    else if change.deletions.count > 0 {
        self.notifyInvalidated()
    }
}

private func updateViewModel() {
    
    guard let object = self.monitoredResults?.firstObject() as? Object else {
        return
    }
    
    if (object as RLMObject).invalidated { // I've never seen this actually happen, but better safe than sorry!
        self.notifyInvalidated()
        return
    }
    
    let viewModel = ViewModel(realmObject: object)
    self.consumer?.consumeNewViewModel(viewModel)
}

private func notifyInvalidated() {
    self.consumer?.monitoredRealmModelWasInvalidated()
}

It’s very straightforward: If there is an insertion or modification to our results, we generate a new view model and pass it to our consumer. If there’s a deletion in the change set, we notify our consumer that the thing they care about has been deleted so it can react appropriately.

Make it Hard to Fail

Much like Dependency Injection, I find MVVM (or at least our application of it) to be a $10 word for $0.05 concept. What we’re really going for here is immutability in an effort to minimize the volatility of the data we’re presenting. In a setup like this, I find that it’s important to make it easy to do the right thing and make doing the wrong thing hard or impossible. As we flesh out the underlying tooling for this pattern, I’m trying to make it as difficult as possible to actually get your hands on a Realm model. If we revisit our example from above, you can see that the only thing that ever gets to touch a real Group model is our view model constructor. As a consequence, our view controller really has no choice but to pass the remoteId around when it needs to interact with other classes.

class PeopleViewController: UIViewController {
	
	private let presentableFactory: RealmObjectPresentableFactory<RDPeopleViewController>

	init(groupId: Int, realm: RLMRealm) {
		self.presentableFactory = RealmObjectPresentableFactory(realm: realm, primaryKeyValue: groupId)
		super.init(nibName: nil, bundle: nil)

		self.presentableFactory.consumer = self
	}
}

extension PeopleViewController: RealmObjectViewModelConsumer {

	typealias ViewModel = PeopleGroupViewModel

	func consumeNewViewModel(model: PeopleGroupViewModel) {
		self.title = model.name
		self.sectionHeader.memberCountLabel.text = "\(model.memberCount)"
	}
}

TL;DR

The biggest issue we continue to struggle with from our Realm migration has been trying to access models that have been deleted from under us. Our strategy for progressively remedying that issue is to leverage some simple concepts from the MVVM pattern and embrace immutability throughout our code base. The north star of this effort is to stop retaining Realm models in our classes, instead passing objects by ID and extracting relevant properties into view models. We’ve established some simple tooling to facilitate on-demand UI updates and make it easy to write safer code. Ultimately, we hope to eliminate this class of error entirely by severely restricting access to actual Realm models to as few classes as possible.

Swift is fantastic.