A practical MVVM example in Swift – Part 1

Datetime:2016-08-23 01:24:43          Topic: Swift  MVVM Model           Share

The good ol’ MVC pattern has been around for a while. The acronym usually stands for Model-View-Controller, but in iOS, we’re mostly referring to Massive-View-Controller and we all know why. 1000+ line view controllers are not uncommon. And why’s that? Probably because it’s too easy to write messy view controllers. Even Apple is famous for not following the MVC pattern in their sample code.

Here’s a lovely scheme I made which shows how MVC should look like:

Here’s what most of the “MVC” implementations end up looking like:

On top of that, the Controller becomes massive, since it’s doing too much work.

But this post is not about MVC. We’ll be looking at MVVM, which stands for Model-View-ViewModel. In theory, it should look like:

It even looks a lot clearer than the first one, right? The Controller now becomes in charge of displaying what the View Model offers and doesn’t care about the Model. Same goes for View – UILabels, UITextFields and UIImageViews will only show what the View Model provides.

What’s the benefit of using this, you might ask.

Well, for example – the Controller will become a lot less bloated . If we want custom formatting, manipulation with the Model data, we can easily do it in the View Model. A typical example is formatting and displaying NSDate. Usually, we’re doing that in the Controller, which means adding unnecessary code to an already bloated class. Instead, the formatting can reside in the View Model – then we know exactly which class/file is responsible for converting NSDate to String, which can then be assigned to a UILabel on the View by the Controller.

Another good reason is that your apps become more testable. You can easily write tests for View Models and specify exactly what you should expect for the user to see. Your Controllers also become more testable, because you’re forced to write more loosely coupled code.

Example

Let’s get down to it!

Model, ViewModel and Unit Test

For our example, we’ll be using a relatively simple class that represents a car. Here’s how it looks like:

class Car {
  var model: String?
  var make: String?
  var horsepower: Int?
  var photoURL: String?
  
  init(model: String, make: String, horsepower: Int, photoURL: String) {
    self.model = model
    self.make = make
    self.horsepower = horsepower
    self.photoURL = photoURL
  }
}

Now, let’s introduce our ViewModel that we’re gonna call CarViewModel (surprise):

class CarViewModel {
  privatevar car: Car?
  var modelText: String?
  var makeText: String?
  var horsepowerText: String?
  var titleText: String?
  var photoURL: NSURL?
  
  init(car: Car) {
    self.car = car
  }
}

Note: I’ve put the CarViewModel in the same Car.swift file to keep everything a bit more confined. You can put your View Models in a separate file.

We’re gonna do some TDD now, so get ready!

A bit earlier, I mentioned that your app becomes more testable so let’s add a test that verifies our CarViewModel is doing its job properly.

Note: I’m using native Unit Testing Case classes

Our first unit test will assert that a Ferrari F12 is properly represented by CarViewModel:

func testCarViewModelWithFerrariF12() {
  let ferrariF12 = Car(model: "F12", make: "Ferrari", horsepower: 730, photoURL: "http://auto.ferrari.com/en_EN/wp-content/uploads/sites/5/2013/07/Ferrari-F12berlinetta.jpg")
  let ferrariViewModel = CarViewModel(car: ferrariF12)
  XCTAssertEqual(ferrariViewModel.modelText, "F12")
  XCTAssertEqual(ferrariViewModel.makeText, "Ferrari")
  XCTAssertEqual(ferrariViewModel.horsepowerText, "730 HP")
  XCTAssertEqual(ferrariViewModel.photoURL, NSURL(string: ferrariF12.photoURL!))
  XCTAssertEqual(ferrariViewModel.titleText, "Ferrari F12")
}

If you press ⌘+U each and every assertion will fail. That’s fine since TDD dictates we should go red/green/refactor.

Now let’s go green (even though we’re dealing with a Ferrari) and fix our CarViewModel:

class CarViewModel {
  privatevar car: Car?
  
  var modelText: String? {
    return car?.model
  }
  
  var makeText: String? {
    return car?.make
  }
  
  var horsepowerText: String? {
    guardlet horsepower = car?.horsepower else {
      return nil
    }
    return "\(horsepower) HP"
  }
  
  var titleText: String? {
    guardlet make = car?.make, model = car?.model else {
      return nil
    }
    return "\(make) \(model)"
  }
  
  var photoURL: NSURL? {
    guardlet photoURL = car?.photoURL else {
      return nil
    }
    return NSURL(string: photoURL)
  }
  
  init(car: Car) {
    self.car = car
  }
}

We can already see the benefits of using a MVVM approach. Our Car class is straightforward as it can be, but the CarViewModel adds a bit of decoration to our plain class. For example, look at the titleText variable. It concatenates the make and model of the car. The horsepowerText adds HP (horsepower) next to the Int horsepower value of the car. The photoURL variable constructs a NSURL we can use to download the actual car photo.

⌘+U and we see our test passing, hooray! Pretty cool, right?

User interface (UITableView)

To further demonstrate MVVM, let’s use an array of cars shown in a UITableView.

First, I’ll add a new class called TableViewController which is a subclass of a UITableViewController.

I started my project as a Single View Application so in the Main.storyboard, I’ll delete the existing View Controller and add a new UITableViewController. I set its class to my TableViewController:

Then, I set the cell’s reuse identifier to CarCell and the style of the cell to Right Detail (so we can display the car’s photo, title, and horsepower).

Time to write some code in our newly created TableViewController.

If we’re following the MVVM pattern down to the t (which we should!), our TableViewController won’t have a clue about our Car model. It only knows the CarViewModel.

In a real world application, we’d be fetching our data from an external source, but for this example, we’ll just let our AppDelegate hold an array of cars.

So, our array of cars (in AppDelegate) is actually an array of CarViewModel objects:

// AppDelegate.swift
let cars: [CarViewModel] = {
  let ferrariF12 = Car(model: "F12", make: "Ferrari", horsepower: 730, photoURL: "http://auto.ferrari.com/en_EN/wp-content/uploads/sites/5/2013/07/Ferrari-F12berlinetta.jpg")
  let zondaF = Car(model: "Zonda F", make: "Pagani", horsepower: 602, photoURL: "http://storage.pagani.com/view/1024/BIG_zg-4-def.jpg")
  let lamboAventador = Car(model: "Aventador", make: "Lamborghini", horsepower: 700, photoURL: "http://cdn.lamborghini.com/content/models/aventador_lp700-4_roadster/gallery_2013/roadster_21.jpg")
  
  return [CarViewModel(car: ferrariF12), CarViewModel(car: zondaF), CarViewModel(car: lamboAventador)]
}()

We have to get a hold of that in our TableViewController:

let cars: [CarViewModel] = (UIApplication.sharedApplication().delegate as! AppDelegate).cars

Two necessary boilerplate functions:

override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
  return 1
}
 
override func tableView(tableView: UITableView, numberOfRowsInSectionsection: Int) -> Int {
  return cars.count
}

And the actual binding part:

override func tableView(tableView: UITableView, cellForRowAtIndexPathindexPath: NSIndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCellWithIdentifier("CarCell", forIndexPath: indexPath)
  let carViewModel = cars[indexPath.row]
  
  cell.textLabel?.text = carViewModel.titleText
  cell.detailTextLabel?.text = carViewModel.horsepowerText
  loadImage(cell, photoURL: carViewModel.photoURL)
  
  return cell
}

The only unknown bit is the loadImage function which is not that important for our sample, but we don’t want to block the main thread while we’re loading image data. At this point, I usually use something like Alamofire to help me with downloading images, but I don’t want to complicate the example too much, so we’re just gonna roll with the basic implementation:

func loadImage(cell: UITableViewCell, photoURL: NSURL?) {
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
    guardlet imageURL = photoURL, imageData = NSData(contentsOfURL: imageURL) else {
      return
    }
    dispatch_async(dispatch_get_main_queue()) {
      cell.imageView?.image = UIImage(data: imageData)
      cell.setNeedsLayout()
    }
  }
}

Note: if you’re following this example, you have to add a key to your Info.plist, otherwise images won’t be loaded from non-secure locations:

<key>NSAppTransportSecurity</key>
<dict>
 <key>NSAllowsArbitraryLoads</key>
 <true/>
</dict>

This concludes our TableViewController. If I run the app, I see that the table view is properly displaying my array of cars (it would be nice if it were an actual array of my cars … in my garage):

To top the first part off, let’s write a simple UI test just to make sure everything works:

class MVVMUITests:XCTestCase {
  
  override func setUp() {
    super.setUp()
    
    // Put setup code here. This method is called before the invocation of each test method in the class.
    
    // In UI tests it is usually best to stop immediately when a failure occurs.
    continueAfterFailure = false
    // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
    XCUIApplication().launch()
    
    // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
  }
  
  override func tearDown() {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    super.tearDown()
  }
  
  func testFerrariF12DataDisplayed() {
    let app = XCUIApplication()
    let table = app.tables.elementBoundByIndex(0)
    
    let ferrariCell = table.cells.elementBoundByIndex(0)
    XCTAssert(ferrariCell.staticTexts["Ferrari F12"].exists)
    XCTAssert(ferrariCell.staticTexts["730 HP"].exists)
    
    let zondaCell = table.cells.elementBoundByIndex(1)
    XCTAssert(zondaCell.staticTexts["Pagani Zonda F"].exists)
    XCTAssert(zondaCell.staticTexts["602 HP"].exists)
    
    let lamboCell = table.cells.elementBoundByIndex(2)
    XCTAssert(lamboCell.staticTexts["Lamborghini Aventador"].exists)
    XCTAssert(lamboCell.staticTexts["700 HP"].exists)
  }
  
}

Everything is A-ok! So why the tests, you might ask?

Drum roll … data source changes!

This was so simple that we have to complicate things a bit to make it interesting.

Let’s say our Car model has to change for some reason. Our data source changed from returning horsepower to returning kilowatts. First, we change the Car model and replace horsepower with kilowatts:

class Car {
  var model: String?
  var make: String?
  var kilowatts: Int?
  var photoURL: String?
  
  init(model: String, make: String, kilowatts: Int, photoURL: String) {
    self.model = model
    self.make = make
    self.kilowatts = kilowatts
    self.photoURL = photoURL
  }
}

We’ll see we have to make some changes the CarViewModel as well, since our tests expect horsepowerText, not kilowattsTexts:

// class CarViewModel {
// ...
static let horsepowerPerKilowatt = 1.34102209
var horsepowerText: String? {
  guardlet kilowatts = car?.kilowatts else {
    return nil
  }
  let horsepower = Int(round(Double(kilowatts) * CarViewModel.horsepowerPerKilowatt))
  return "\(horsepower) HP"
}
// ...
// }

Of course, we have to change the initializers throughout the app, otherwise we’ll get build errors (since Car’s init function changed to accept kilowatts instead of horsepower):

// AppDelegate.swift
let cars: [CarViewModel] = {
  let ferrariF12 = Car(model: "F12", make: "Ferrari", kilowatts: 544, photoURL: "http://auto.ferrari.com/en_EN/wp-content/uploads/sites/5/2013/07/Ferrari-F12berlinetta.jpg")
  let zondaF = Car(model: "Zonda F", make: "Pagani", kilowatts: 449, photoURL: "http://storage.pagani.com/view/1024/BIG_zg-4-def.jpg")
  let lamboAventador = Car(model: "Aventador", make: "Lamborghini", kilowatts: 522, photoURL: "http://cdn.lamborghini.com/content/models/aventador_lp700-4_roadster/gallery_2013/roadster_21.jpg")
  
  return [CarViewModel(car: ferrariF12), CarViewModel(car: zondaF), CarViewModel(car: lamboAventador)]
}()
 
// MVVMTests.swift
let ferrariF12 = Car(model: "F12", make: "Ferrari", kilowatts: 544, photoURL: "http://auto.ferrari.com/en_EN/wp-content/uploads/sites/5/2013/07/Ferrari-F12berlinetta.jpg")

If we run tests (⌘+U), we’ll see that everything still works (unit and UI tests) and although our data source changed, our Controller (TableViewController) stayed the same, since it really doesn’t care about the Model. As long as the ViewModel (CarViewModel) has the expected variables, we’re fine.

The fully working project is waiting for you on GitHub . (You can also download a zipped version here )

Next time …

Most of the apps require some kind of user interaction, which means that your Controller will also be responsible for passing updates back to the View Model, which will have to update the Model itself. This is a perfect job for a binding mechanism. We’re going to cover this in part 2 so make sure you leave your email and get notified when it goes live.

Thanks for reading this!





About List