In my previous post in this series, I wrote about the Model-View-Controller pattern and some of its imperfections. Despite the clear benefits MVC brings to software development, it tends to fall short in large or complex Cocoa applications.
This isn't news, though. Several architectural patterns have emerged over the years, aiming to address the shortcomings of the Model-View-Controller pattern. You may have heard of MVP, Model-View-Presenter, and MVVM, Model-View-ViewModel, for example. These patterns look and feel similar to the Model-View-Controller pattern, but they also tackle some of the issues the Model-View-Controller pattern suffers from.
1. Why Model-View-ViewModel
I had been using the Model-View-Controller pattern for years before I accidentally stumbled upon the Model-View-ViewModel pattern. It's not surprising that MVVM is a latecomer to the Cocoa community, since its origins lead back to Microsoft. However, the MVVM pattern has been ported to Cocoa and adapted to the requirements and needs of the Cocoa frameworks and has recently been gaining traction in the Cocoa community.
Most appealing is how MVVM feels like an improved version of the Model-View-Controller pattern. This means that it doesn't require a dramatic change of mindset. In fact, once you understand the fundamentals of the pattern, it's fairly easy to implement, no more difficult than implementing the Model-View-Controller pattern.
2. Putting View Controllers on a Diet
In the previous post, I wrote that the controllers in a typical Cocoa application are a bit different from the controllers Reenskaug defined in the original MVC pattern. On iOS, for example, a view controller controls a view. Its sole responsibility is populating the view it manages and responding to user interaction. But that's not the only responsibility of view controllers in most iOS applications, is it?
The MVVM pattern introduces a fourth component to the mix, the view model, which helps refocus the view controller. It does this by taking over some of the responsibilities of the view controller. Take a look at the diagram below to better understand how the view model fits into the Model-View-ViewModel pattern.
As the diagram illustrates, the view controller no longer owns the model. It's the view model that owns the model, and the view controller asks the view model for the data it needs to display.
This is an important difference from the Model-View-Controller pattern. The view controller has no direct access to the model. The view model hands the view controller the data it needs to display in its view.
The relationship between the view controller and its view remains unchanged. That's important because it means that the view controller can focus exclusively on populating its view and handling user interaction. That's what the view controller was designed for.
The result is pretty dramatic. The view controller is put on a diet, and many responsibilities are shifted to the view model. You no longer end up with a view controller that spans hundreds or even thousands of lines of code.
3. Responsibilities of the View Model
You're probably wondering how the view model fits into the bigger picture. What are the tasks of the view model? How does it relate to the view controller? And what about the model?
The diagram I showed you earlier gives us a few hints. Let's start with the model. The model is no longer owned by the view controller. The view model owns the model, and it acts as a proxy to the view controller. Whenever the view controller needs a piece of data from its view model, the latter asks its model for the raw data and formats it in such a way that the view controller can immediately use it in its view. The view controller isn't responsible for data manipulation and formatting.
The diagram also reveals that the model is owned by the view model, not the view controller. It's also worth pointing out that the Model-View-ViewModel pattern respects the close relationship of the view controller and its view, which is characteristic for Cocoa applications. That's why MVVM feels like a natural fit for Cocoa applications.
4. An Example
Because the Model-View-ViewModel pattern is not native to Cocoa, there are no strict rules to implement the pattern. Unfortunately, this is something many developers get confused by. To clarify a few things, I'd like to show you a basic example of an application that uses the MVVM pattern. We create a very simple application that fetches weather data for a predefined location from the Dark Sky API and displays the current temperature to the user.
Step 1: Set Up the Project
Fire up Xcode and create a new project based on the Single View Application template. I'm using Xcode 8 and Swift 3 for this tutorial.
Name the project MVVM, and set Language to Swift and Devices to iPhone.
Step 2: Create a View Model
In a typical Cocoa application powered by the Model-View-Controller pattern, the view controller would be in charge of performing the network request. You could use a manager for performing the network request, but the view controller would still know about the origins of the weather data. More importantly, it would receive the raw data and would need to format it before displaying it to the user. This isn't the approach we take when adopting the Model-View-ViewModel pattern.
Let's create a view model. Create a new Swift file, name it WeatherViewViewModel.swift, and define a class named WeatherViewViewModel
.
import Foundation class WeatherViewViewModel { }
The idea is simple. The view controller asks the view model for the current temperature for a predefined location. Because the view model sends a network request to the Dark Sky API, the method accepts a closure, which is invoked when the view model has data for the view controller. That data could be the current temperature, but it could also be an error message. This is what the currentTemperature(completion:)
method of the view model looks like. We'll fill in the details in a few moments.
import Foundation class WeatherViewViewModel { // MARK: - Type Alias typealias CurrentTemperatureCompletion = (String) -> Void // MARK: - Public API func currentTemperature(completion: @escaping CurrentTemperatureCompletion) { } }
We declare a type alias for convenience and define a method, currentTemperature(completion:)
, that accepts a closure of type CurrentTemperatureCompletion
.
The implementation isn't hard if you're familiar with networking and the URLSession
API. Take a look at the code below and notice that I've used an enum, API
, to keep everything nice and tidy.
import Foundation class WeatherViewViewModel { // MARK: - Type Alias typealias CurrentTemperatureCompletion = (String) -> Void // MARK: - API enum API { static let lat = 37.8267 static let long = -122.4233 static let APIKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" static let baseURL = URL(string: "https://api.darksky.net/forecast")! static var requestURL: URL { return API.baseURL .appendingPathComponent(API.APIKey) .appendingPathComponent("\(lat),\(long)") } } // MARK: - Public API func currentTemperature(completion: @escaping CurrentTemperatureCompletion) { let dataTask = URLSession.shared.dataTask(with: API.requestURL) { [weak self] (data, response, error) in // Helpers var formattedTemperature: String? if let data = data { formattedTemperature = self?.temperature(from: data) } DispatchQueue.main.async { completion(formattedTemperature ?? "Unable to Fetch Weather Data") } } // Resume Data Task dataTask.resume() } }
The only piece of code that I haven't showed you yet is the implementation of the temperature(from:)
method. In this method, we extract the current temperature from the Dark Sky response.
// MARK: - Helper Methods func temperature(from data: Data) -> String? { guard let JSON = try? JSONSerialization.jsonObject(with: data, options: []) as? [String : Any] else { return nil } guard let currently = JSON?["currently"] as? [String : Any] else { return nil } guard let temperature = currently["temperature"] as? Double else { return nil } return String(format: "%.0f °F", temperature) }
In a production application, I'd opt for a more robust solution to parse the response, such as ObjectMapper or Unbox.
Step 3: Integrate the View Model
We can now use the view model in the view controller. We create a property for the view model, and we also define three outlets for the user interface.
import UIKit class ViewController: UIViewController { // MARK: - Properties @IBOutlet var temperatureLabel: UILabel! // MARK: - @IBOutlet var fetchWeatherDataButton: UIButton! // MARK: - @IBOutlet var activityIndicatorView: UIActivityIndicatorView! // MARK: - private let viewModel = WeatherViewViewModel() }
Notice that the view controller owns the view model. In this example, the view controller is also responsible for instantiating its view model. In general, I prefer to inject the view model into the view controller, but let's keep it simple for now.
In the view controller's viewDidLoad()
method, we invoke a helper method, fetchWeatherData()
.
// MARK: - View Life Cycle override func viewDidLoad() { super.viewDidLoad() // Fetch Weather Data fetchWeatherData() }
In fetchWeatherData()
, we ask the view model for the current temperature. Before we request the temperature, we'll hide the label and button and show the activity indicator view. In the closure we pass to fetchWeatherData(completion:)
, we update the user interface by populating the temperature label and hiding the activity indicator view.
// MARK: - Helper Methods private func fetchWeatherData() { // Hide User Interface temperatureLabel.isHidden = true fetchWeatherDataButton.isHidden = true // Show Activity Indicator View activityIndicatorView.startAnimating() // Fetch Weather Data viewModel.currentTemperature { [unowned self] (temperature) in // Update Temperature Label self.temperatureLabel.text = temperature self.temperatureLabel.isHidden = false // Show Fetch Weather Data Button self.fetchWeatherDataButton.isHidden = false // Hide Activity Indicator View self.activityIndicatorView.stopAnimating() } }
The button is wired to an action, fetchWeatherData(_:)
, in which we also invoke the fetchWeatherData()
helper method. As you can see, the helper method helps us avoid code duplication.
// MARK: - Actions @IBAction func fetchWeatherData(_ sender: Any) { // Fetch Weather Data fetchWeatherData() }
Step 4: Create the User Interface
The last piece of the puzzle is to create the user interface of the example application. Open Main.storyboard and add a label and a button to a vertical stack view. We'll also add an activity indicator view on top of the stack view, centered vertically and horizontally.
Don't forget to wire up the outlets and the action we defined in the ViewController
class!
Now build and run the application to give it a try. Remember that you need a Dark Sky API key to make the application work. You can sign up for a free account on the Dark Sky website.
5. What Are the Benefits?
Even though we only moved a few bits and pieces to the view model, you may be wondering why this is necessary. What did we gain? Why would you add this additional layer of complexity?
The most obvious gain is that the view controller is leaner and more focused on managing its view. That's the core task of a view controller: managing its view.
But there's a more subtle benefit. Because the view controller isn't responsible for fetching the weather data from the Dark Sky API, it isn't aware of the details related to this task. The weather data could come from a different weather service or from a cached response. The view controller wouldn't know, and it doesn't need to know.
Testing also improves dramatically. View controllers are known to be hard to test because of their close relationship to the view layer. By moving some of the business logic to the view model, we instantly improve the testability of the project. Testing view models is surprisingly easy because they don't have a link to the application's view layer.
Conclusion
The Model-View-ViewModel pattern is a significant step forward in designing Cocoa applications. View controllers aren't so massive, view models are easier to compose and test, and your project becomes more manageable as a result.
In this short series, we only scratched the surface. There's a lot more to write about the Model-View-ViewModel pattern. It's become one of my favorite patterns over the years, and that's why I keep speaking and writing about it. Give it a try and let me know what you think!
In the meantime, check out some of our other posts about Swift and iOS app development.
- SwiftWhat's New in Swift 4
- iOS SDKFaster Logins With Password AutoFill in iOS 11
- Mobile DevelopmentHow to Submit an iOS App to the App Store