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 .
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
iOS MVVM Tutorial: Refactoring from MVC
25 mins
- Getting Started
- Introducing MVVM Roles and Responsibilities
- Becoming Familiar With the Existing App Structure
- WeatherViewController
- Data Binding Using Box
- Creating WeatherViewModel
- Formatting Data in MVVM
- Adding Functionality in MVVM
- Unit Testing With MVVM
- Reviewing The Refactoring to MVVM
- Where to Go From Here?
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.
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:
- Create a
UIAlertController
with a text field. - Add an action button for Submit. The action passes the new location string to
viewModel.changeLocation(to:)
. - Present the
alert
.
Build and run.
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.
- The
locationName
binding is asynchronous. Use anexpectation
to wait for the asynchronous event. - Create an instance of
viewModel
to test. - 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. - 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. - 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.
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 theWeatherViewModel
by sending inputs, such aschangeLocation(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.
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!