iOS MVVM Tutorial: Refactoring from MVC

In this iOS tutorial, you’ll learn how to convert an MVC app into MVVM. In addition, you’ll learn about the components and advantages of using MVVM. By Chuck Krutsinger .

4.8 (70) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 3 of this article. Click here to view the first page.

Adding Functionality in MVVM

So far, you can check the weather for your default location. But what if you want to know the weather somewhere else? You can use MVVM to add a button to check the weather at other locations.

You may have noticed the location symbol ➤ in the upper left corner. It’s a button that doesn’t work, yet. Next, you’ll hook that to an alert that prompts for a new location and then fetches the weather for that new location.

First, open Weather.storyboard. Then, open WeatherViewController.swift in the assistant editor.

Next, control-drag Change Location Button to the end of WeatherViewController. Name the method promptForLocation.

Gif showing user opening Weather.storyboard, WeatherViewController.swift, drawing Change location button and naming the method promptForLocation

Now add the following code to promptForLocation(_:):

//1
let alert = UIAlertController(
  title: "Choose location",
  message: nil,
  preferredStyle: .alert)
alert.addTextField()
//2
let submitAction = UIAlertAction(
  title: "Submit", 
  style: .default) { [unowned alert, weak self] _ in
    guard let newLocation = alert.textFields?.first?.text else { return }
    self?.viewModel.changeLocation(to: newLocation)
}
alert.addAction(submitAction)
//3
present(alert, animated: true)

Here’s a breakdown of this method:

  1. Create a UIAlertController with a text field.
  2. Add an action button for Submit. The action passes the new location string to viewModel.changeLocation(to:).
  3. Present the alert.

Build and run.

Gif showing user selecting a new locations, Paris, France and then ggggg

Put in some different locations. You can try Paris, France or Paris, Texas. You can even put in some nonsense such as ggggg to see how the app responds.

Take a moment to reflect on how little code was needed in the view controller to add this new functionality. A single call to the view model triggers the flow for updating the weather data for the location. Smart, right?

Next, you’ll learn how to use MVVM to create unit tests.

Unit Testing With MVVM

One of MVVM’s big advantages is how much easier it makes creating automated tests.

To test a view controller with MVC, you must use UIKit to instantiate the view controller. Then, you have to search through the view hierarchy to trigger actions and verify results.

With MVVM, you write more conventional tests. You may still need to wait for some asynchronous events, but most things are easy to trigger and verify.

To see how much simpler MVVM makes testing a view model, you’ll create a test that makes WeatherViewModel change the location and then confirms that locationName binding updates to the expected location.

First, under the MVVMFromMVCTests group, create a new Unit Test Case Class file named WeatherViewModelTests.

You must import the app for testing. Immediately below import XCTest, add the following:

@testable import Grados

Now, add the following method to WeatherViewModelTests:

func testChangeLocationUpdatesLocationName() {
  // 1
  let expectation = self.expectation(
    description: "Find location using geocoder")
  // 2
  let viewModel = WeatherViewModel()
  // 3
  viewModel.locationName.bind {
    if $0.caseInsensitiveCompare("Richmond, VA") == .orderedSame {
      expectation.fulfill()
    }
  }
  // 4
  DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    viewModel.changeLocation(to: "Richmond, VA")
  }
  // 5
  waitForExpectations(timeout: 8, handler: nil)
}

Here’s an explanation of the new test:

When it creates the test instance of the view model, it also triggers a geocoder lookup. Waiting a few seconds allows those other lookups to complete before triggering the test lookup.

Apple’s documentation explicitly warns that CLLocation can throw an error if the rate of requests is too high.

  1. The locationName binding is asynchronous. Use an expectation to wait for the asynchronous event.
  2. Create an instance of viewModel to test.
  3. Bind to locationName and only fulfill the expectation if the value matches the expected result. Ignore any location name values such as “Loading…” or the default address. Only the expected result should fulfill the test expectation.
  4. Begin the test by changing the location. It’s important to wait a few seconds before making the change so that any pending geocoding activity completes first. When the app launches, it triggers a geocoder lookup.

    When it creates the test instance of the view model, it also triggers a geocoder lookup. Waiting a few seconds allows those other lookups to complete before triggering the test lookup.

    Apple’s documentation explicitly warns that CLLocation can throw an error if the rate of requests is too high.

  5. Wait for up to eight seconds for the expectation to fulfill. The test only succeeds if the expected result arrives before the timeout.

Click the diamond next to testChangeLocationUpdatesLocationName() to run the test. When the test passes, the diamond will turn to a green checkmark.

Gif showing user running the test

From here, you can follow this example to create tests that confirm the other values for WeatherViewModel. Ideally, you would inject a mock weather service to remove the dependency on weatherbit.io for the tests.

Reviewing The Refactoring to MVVM

Good job getting this far! As you look back over the changes, you can see some of the benefits of MVVM that resulted from the refactoring:

  • Reduced complexity: WeatherViewController is now much simpler.
  • Specialized: WeatherViewController no longer depends on any model types and only focuses on the view.
  • Separated: WeatherViewController only interacts with the WeatherViewModel by sending inputs, such as changeLocation(to:), or binding to its outputs.
  • Expressive: WeatherViewModel separates the business logic from the low level view logic.
  • Maintainable: It’s simple to add a new feature with minimal modification to the WeatherViewController.
  • Testable: The WeatherViewModel is relatively easy to test.

However, there are some trade-offs to MVVM that you should consider:

  • Extra type: MVVM introduces an extra view model type to the structure of the app.
  • Binding mechanism: It requires some means of data binding, in this case the Box type.
  • Boilerplate: You need some extra boilerplate to implement MVVM.
  • Memory: You must be conscious of memory management and memory retain cycles when introducing the view model into the mix.

Where to Go From Here?

You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.

MVVM has become a core competency for professional iOS developers. In many professional settings, you should be familiar with MVVM and be able to implement it. This is especially true given Apple’s introduction of the Combine framework, which enables reactive programming.

The Design Patterns By Tutorials book is a great source for more on the MVVM pattern.

Cover Art for Design Patterns by Tutorials: Coral reef on a blue background

If you want to learn more about the Combine framework and how to implement MVVM using Combine, check out this tutorial on MVVM with Combine or the Combine: Asynchronous Programming With Swift book.

For more on Key-Value Observing, check out What’s New in Foundation: Key-Value Observing.

I hope you’ve enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!