MVVM, Protocols, and Operations in Swift

Introduction

Model-View-ViewModel (MVVM) is a design pattern for building GUI Apps that is similar to Model-View-Controller (MVC). MVVM has a number of advantages over MVC when building Swift Apps. This advantage is multiplied when combined with Protocols and the Operations pattern.

History

MVC is a design pattern that was originally invented at Xerox Parc in the Smalltalk programming language. Objective-C has a rich history of inspiration by Smalltalk. As such, Cocoa is also an MVC framework.

MVC gained even more popularity on the web with WebObjects, Ruby on Rails, and Django.

Martin Fowler described Presentation Model as an alternative to MVC in the early 2000s. Presentation Model is a design pattern that represents the state and behaviours of the presentation of a GUI independently of the GUI’s controls.

MVVM is a variation of Presentation Model. It also recognizes that state and behaviour should exist independent of controls. MVVM simplifies event-driven GUIs by adding the concept of Data Bindings.

Protocol-Oriented Programming (POP) was popularized by a presentation at WWDC in 2015. POP is the idea that value-types, interfaces, and explicit ownership is better than the reference-types, class hierarchies, and the implicit data ownership that classes provide.

Motivation

MVC frameworks tend to lead to Apps with Massive View Controllers™. This is because Controllers end up with more than a single responsibility.

Network calls, persistence, view logic, GPS handling, and all other sorts of code ends up in the Controller.

This makes Controller code difficult to read, difficult to change, and difficult to unit test. This leads to buggy Apps and a development pace that slows to a crawl.

Building Apps with MVVM, Operations, and Protocols leads to:

  1. Separation of concerns. Each component has a single responsibility.
  2. Communication through interfaces. Protocols specify interfaces that make communication explicit.
  3. Explicit data sharing. Value-types are copied whereas reference-types implicitly share data.
  4. Modularity. The network, persistence, and other layers can be modularized for reuse.
  5. Scalability. Modularization allows for team size scaling as each module can be owned by a different developer or team.
  6. Proper tooling. Views can be created in Interface Builder and injected as dependencies into their Controller.
  7. Unit testing. Each component can be instantiated individually. Protocols express explicit interfaces which means dependencies don’t have to be mocked. This makes code more testable.

How MVVM Works

The MVVM pattern consists of three main components: Views, Models, and ViewModels. Data Binding and interaction between components is specified by Protocols. Operations encapsulate events. Controllers are the fourth component that tie all of the above together.

View

Views are created and injected into Controllers using Interface Builder. This is also true of the classic MVC design pattern that Cocoa developers are familiar with.

init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: NSBundle?)

The framework binds Views to variables in the Controller.

class ViewController: UIViewController {
  @IBOutlet weak var anotherView: UIView!
}

It is common to load a UITableViewCell class from an XIB file.

class CustomCell: UITableViewCell {
  @IBOutlet weak var label: UILabel!
}

class ViewController: UITableViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    let nib = UINib(nibName: "CustomCell", bundle: nil)
    tableView.registerNib(nib, forCellReuseIdentifier: "CustomCellID")
  }
  
  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("CustomCell", forIndexPath: indexPath)
    return cell
  }
}

The View should not contain view logic and it certainly should not contain business logic.

Model

The term Model is short for data model or domain model. Business logic should lie in the Model.

Model objects inherit from NSObject in Objective-C. They may also inherit from NSManagedObject if they are persisted by Core Data. Model objects are reference types in these two cases.

// Plain old Objective-C object
class User: NSObject {
  var firstName: String?
  var lastName: String?
}

// Core Data object
class User: NSManagedObject {
  @NSManaged weak var firstName: String?
  @NSManaged weak var lastName: String?
}

Model objects can be value types in Swift. They can be structs instead of classes.

struct User {
  let firstName: String
  let lastName: String
}

Model objects represent state in an application. This state is persisted between runs of the application.

The interface of a Model object should not contain any View, ViewModel, or Controller types. A Model object should not own any of these types. No member function should take any of these types as parameters. There should be no import statements for these types.

ViewModel

A ViewModel processes data from a Model for a View. A ViewModel’s single responsibility is view logic.

struct UserViewModel {
  let fullName: String
  
  init(user: User) {
    fullName = "\(user.firstName) \(user.lastName)"
  }
}

The ViewModel encapsulates view state. This view state is transient and does not necessarily need to be persisted between runs of the application.

This transient state can be more than just data formatting of a Model object. For example, the ViewModel can store loading state.

enum LoadingState {
  case Blank
  case Loading
  case Loaded
  case Error
}

extension UserViewModel {
  var loadingState = LoadingState.Blank
}

Controllers

Controllers are responsible for managing the interaction of components.

Views, ViewModels, and Models are injected into Controllers. Controllers wire these components together. Protocols define this wiring.

func makeUserViewController() -> UIViewController {
  let model = User()
  let viewModel = UserViewModel(user: model)
  let vc = UserViewController(nibName: "UserViewController", bundle: nil, viewModel: viewModel)
  return vc
}

Controllers also instantiate Operations in response to events. These events may be generated by UI, timers, hardware sensors, or network calls.

UI events, known as actions in Cocoa, are bound to methods by using the @IBAction mechanism in Interface Builder.

class ViewController: UIViewController {
  @IBAction func submitButtonPressed(sender: UIButton) {  
    let operation = SubmitOperation(data: self.model)
    operation.execute() { (result, error) in
    }
  }
}

Data Binding

Data Binding is a technique that connects a data source to a consumer. The goal of binding is the exchange of data. A View should be notified when a Model changes. And a Model should be notified when a View changes.

@IBOutlet binds variables of type UIView. It can also bind types that conform to Protocols. A common example of this is UITableViewDataSource and UITableViewDelegate.

@IBAction binds a method to a control’s action as we saw above.

These however are not the only options for binding data.

Functions are first-class in Swift. This means functions can be arguments to functions, functions can be return values, and functions can be stored in variables.

A closure is a stored function that has access to scope outside of itself.

Updating a Model in response to an operation is excellent usage of a closure.

  @IBAction func submitButtonPressed(sender: UIButton) {  
    let operation = SubmitOperation(data: self.model)
    operation.execute() { (result, error) in
      updateModel(result, self.model)
    }
  }

Dependency Injection

Dependency Injection (DI) is a design pattern where the services that a client requires are passed as parameters to the client rather than being built or imported.

The simplest way to achieve this in Swift is to have a function that builds parameters and passes them to the Controller in its init function.

func makeUserViewController() -> UIViewController {
  let model = User()
  let viewModel = UserViewModel(user: model)
  let vc = UserViewController(nibName: "UserViewController", bundle: nil, viewModel: viewModel)
  return vc
}

That is it. That is all. That is the simplest case of DI.

DI has a poor reputation because of complicated DI frameworks in legacy object-oriented programming languages. These complicated DI frameworks make code dependent on the DI framework itself. This is counter to the goal of DI.

DI should make code simpler by allowing each type to have a single responsibility that is easy to debug and unit test.

There are going to be at least two configuration points for a Controller. The previous code snippet outlines App code. The second configuration point is going to be unit tests.

func testUser() {
  let fakeUser = FakeUser()
  let viewModel = UserViewModel(user: fakeUser)
  let vc = UserViewController(nibName: "UserViewController", bundle: nil, viewModel: viewModel)
  
  // Assert state and behaviours
}

Operations

Operations encapsulate network, persistence, hardware, and other logic and isolate it out of Controller code.

At minimum, an Operation has an init function that takes the state required to execute the operation and a method to kick off execution.

struct SubmitOperation {
  let user: User
  init(user: User) {
    self.user = user
  }
  
  func execute(handler: (Response, ErrorType) -> ()) {
    // ...
  }
}

Operations are sometimes called Commands in other programming languages. The NSOperation class in Cocoa provides support for creating operations. It abstracts away some execution logic and the ability to execute asynchronously.

class Operation: NSOperation {
  // ...
}

The counterpart to an operation is a queue. Queues allow operations run asynchronously. Dependent operations can be run serially. Operations can be rate-limited.

Grand Central Dispatch is another API that is useful when creating Operations.

struct Operation {
  let queue: dispatch_queue_t
  
  func execute() {
    dispatch_async(queue) {
      // ...
    }
  }
}

Networking

Networking code is an excellent candidate to wrap up in Operations.

It is inherently asynchronous, difficult to debug, prone to poor network conditions, and has a tendency to muck up Controller code.

Core Data

Persistence is also an excellent candidate to wrap up in Operations.

NSFetchedResultsController is an excellent abstraction as a data source for a UITableViewController.

NSFetchRequest is also an excellent example of the Operation pattern at work.

Writing an NSManagedObject to an NSManagedObjectContext is an excellent case for writing an Operation.

Protocols

A class, struct, or enum can adopt a Protocol. A Protocol defines the methods, properties, and other requirements that a type should conform to.

protocol User {
}

protocol Administrable {
}

struct Person: User {
}

struct Admin: User, Administrable {
}

struct FakeUser: UserProtocol {
}

Interfaces should depend on Protocols, not on concrete types.

struct UserViewModel {
  init(user: User)
}

let model = Admin()
let viewModel = UserViewModel(user: model)

Protocols are an example of The Dependency Inversion Principle in practice. This principle states that:

Protocol Oriented Programming vs Object Oriented Programming

Protocol Oriented Programming is distinct from Object Oriented Programming in a few key ways:

Value-types copy state rather than share state with collaborating types. This leads to explicit data sharing. Shared state can be changed by one object without the knowledge or consent of other objects that also depend on that state.

Protocol Oriented Programming leads to clear type relationships. Object Oriented Programming may lead to classes that inherit from classes that inherit from classes and on and on and on every time additional behaviour needs to be implemented.

Conforming to Protocols leads to static type relationships. This is preferable to casting objects at runtime with as!. as! is a signal that some type information has been lost in the process.

Protocols can also be used to define small, single responsibility, abstractions. This makes client code cleaner to read and to implement.

Unit Testing

It is much easier to unit test MVVM code that is written with Protocols and Operations than it is to test MVC code.

The first reason is that each abstraction really just has a single responsibility. And this responsibility is exposed through an explicit interface.

The second reason is that code dependencies can be injected into the code under test. This isolates the code under test.

The third reason is that Fakes are simpler than Stubs or Mocks for testing.

A Fake is a working implementation of a dependency that takes a short cut to make it easier to test. This may involve keeping state in memory rather than persisting to disk or printing to a console rather than rendering a view. Here is an example of a Fake from Protocol-Oriented Programming from WWDC 2015:

struct TestRenderer: Renderer {
  func moveTo(p: CGPoint) { print("moveTo(\(p.x), \(p.y))) }
  func lineTo(p: CGPoint) { print("lineTo(\(p.x), \(p.y))) }
}

A Stub returns constants as canned answers for the explicit point of testing. A Stub may increment simpler counters or save off strings for testing.

Mocks are more complex than Fakes or Stubs. They keep internal state to verify that certain actions were taken during a test. Here is an example of OCMock:

// create a mock for the user defaults and make sure it's used
id userDefaultsMock = OCMClassMock([NSUserDefaults class]);
OCMStub([userDefaultsMock standardUserDefaults]).andReturn(userDefaultsMock);

// call the code under test
[myController updateUserDefaults];

// verify it has called the expected method
OCMVerify([userDefaultsMock setObject:@"http://someurl" forKey:@"MyAppURLKey"]);

Benefits

In conclusion, MVVM with Protocols and Operations is a much better option than vanilla MVC.

It allows for modularization. Code can be abstracted into types with single responsibilities. This makes the code easier to test. Testable code is easier to change. Code that is easier to change improves development pace.

References