View models

By Will Braynen

The problem

You need to build this user interface, showing to the user how much they have read this year so far:

ui0.png

Yes, it’s a little sparse. But that’s all Product and Design (e.g. the product owner and the designer) want this screen to show. Ideally, on a simple GET request, for this screenful your backend service would return this:

// Data in JSON
{
  "totalBooks": 3,
  "totalPages": 792
}
// The data model in Swift
struct Response: Codable {
  let totalBooks: UInt
  let totalPages: UInt
}

The backend service, however, returns this instead:

[
  {
    "numberOfPages": 448,
    "title": "Master and Margarita",
    "author": "Bulgakov"
  },
  {
    "numberOfPages": 304,
    "title": "Evgeny Onegin",
    "author": "Pushkin"
  },
  {
    "numberOfPages": 40,
    "title": "The Nose",
    "author": "Gogol"
  }
]
// The data model
typealias Response = [Book]
struct Book: Codable {
  let numberOfPages: UInt
  let title: String
  let author: String
}

There is a gap here between what the service returns and what the client needs. In particular, there is a gap between the service model and the model the view actually needs. A view model (as opposed to a service model) helps bridge this gap.

Adding view models can also help thin out view controllers while keeping your views “dumb” so that their only responsibility is managing the UI (e.g. constraints, etc). This, in turn, can help get better code coverage where it matters most. A gap between the service model and the view’s needs is not the only reason to have a view model. But it is the one I will use in this post to motivate having one.

The View-Model Solution

Make the model you wish you had. For example, in Swift, given our earlier example, you would write this model:

/// Not a view model, but much closer to the view's needs.
struct Model {
  let totalBooks: UInt
  let totalPages: UInt
}

Or, better yet, what our view really needs is this:

/// A true view model.
struct ViewModel {
  let titleDisplayText: String
  let totalBooksDisplayText: String
  let totalPagesDisplayText: String
}

We then have the following situation, where the view model acts as the glue between the service model and the view, and where data flows from left to right:

Next, you would add a constructor/initializer for this view model:

/// Some engineers call this "an adapter".
extension ViewModel {
  /// Initializes the view's model from the service model.
  init(from model: Response) {
    let totalBooks = model.books.count
    let totalPages = model.books.map{$0.numberOfPages}.reduce(0,+)

    // TODO: Localize
    self.titleDisplayText = "Total read:"
    self.totalBooksDisplayText = "\(totalBooks) books"
    self.totalPagesDisplayText = "\(totalPages) pages"
  }
}

view-model files

I’ve seen view models added to an Xcode project using two different conventions: (1) keep the view model and its initializer together in one file, or (2) keep the view model with the view, but place the view model’s initializer in a separate file (and call it an “adapter”). For example, assume the name of your view is ReadingStatsView, with Readings.swift housing your service model.

Option 1.

Option 1.

Option 2.

Option 2.

Option 1.

The first convention is then to create a view model called ReadingStatsViewModel (that’s the struct) and place it inside a file bearing its name: ReadingStatsViewModel.swift. Here’s what it would have in it:

//
// ReadingStatsViewModel.swift
//

struct ReadingStatsViewModel {
  let titleDisplayText: String
  let totalBooksDisplayText: String
  let totalPagesDisplayText: String

  /// Initializes the view's model from the service model.
  init(from model: Response) {
    let totalBooks = model.books.count
    let totalPages = model.books.map{$0.numberOfPages}.reduce(0,+)

    // TODO: Localize
    self.titleDisplayText = "Total read:"
    self.totalBooksDisplayText = "\(totalBooks) books"
    self.totalPagesDisplayText = "\(totalPages) pages"
  }
}

Option 2.

The second convention is to create a view model called Model and namespace it inside the view, like so:

class ReadingStatsView {
  struct Model {
    let titleDisplayText: String
    let totalBooksDisplayText: String
    let totalPagesDisplayText: String
  }
}

This means that there would be no file ReadingStatsViewModel.swift. Instead, you would add a file called ReadingStatsViewModelAdapter.swift and place the view model’s initializer there:

//
// ReadingStatsViewModelAdapter.swift
//

extension ReadingStatsView.Model {
  /// Initializes the view's model from the service model.
  init(from model: Response) {
    let totalBooks = model.books.count
    let totalPages = model.books.map{$0.numberOfPages}.reduce(0,+)

    // TODO: Localize
    self.titleDisplayText = "Total read:"
    self.totalBooksDisplayText = "\(totalBooks) books"
    self.totalPagesDisplayText = "\(totalPages) pages"
  }
}

view controller’s responsibility

The view controller owns the views and owns those views’ models—the view models. The view controller then calls the view’s populate method, passing the necessary data to the view via a view model.

let viewModel = ViewModel(from: serviceModel)
view.populate(with: viewModel)

More broadly, the view controller is the conductor of this orchestra. The view controller initiates the network call to the service, receives the service response, instantiates a view model from the service response, and then populates the view with the view model.

View’s responsibility

The view is responsible for laying out the UI components (e.g. adding or activating constraints) and delegating the handling of user events (e.g. button clicks) out to the view controller. The view is not responsible for business logic or for formatting datetimes or monetary currencies—the view model should take care of those things if the backend doesn’t.

Here is the kind of method you would add to your view, so that your view can ingest a view model:

/// Updates the view with the new data
func populate(with model: ViewModel) {
  titleLabel.text = model.titleDisplayText
  totalBooksLabel.text = model.totalBooksDisplayText
  totalPagesLabel.text = model.totalPagesDisplayText
}

This populate method is called from/by the view controller.

Is this mvvm?

You might wonder: what’s this pattern called? After all, it’s comforting when things have names. Perhaps in contrast to MVC (Model-View-Controller, where ‘Model’ refers to service data models and ‘Controller’ refers to view controllers), many people do indeed refer to this pattern as ‘MVVM’ (Model-View-ViewModel).

I am one of these people. I and my kind insist on calling this client-side architectural pattern ‘MVVM’ even though it still comes with a controller. My kind and I think of MVVM as shorthand for MVCVM+. As in, Model+View+Controller+ViewModel plus whatever else there may be. Because MVVM is hard enough to say as is.

But I have also met some engineers who reserve the acronym MVVM for reactive programming. Because bindings. Others differentiate between ‘reactive MVVM’ and plain-old MVVM. Either way, MVVM is a gateway drug to reactive programming—I’ll admit that much. There also those who call it a ‘presentation model’ (because MVVM is a particular reincarnation of the presentation model?). Some even say this is an application of the Adapter pattern, which is right except that it is unclear if it’s just the view model’s initializer that’s the adapter (which is how some of my colleagues conceptualize it) or if the entire view model is the adapter (which is perhaps how I conceptualize it).

Whatever we call it, there is a view model—one view model per view. So perhaps we should just say that we are using view models and leave fancy names out of it.