In Mastering Model-View-ViewModel With Swift, we explore how you can use the Model-View-ViewModel pattern to simplify table views. The example we use in the course is Cloudy, a weather application powered by the Dark Sky API. The settings view of the application contains a handful of settings.
The implementation of the view controller that drives the settings view looks very typical for an application built with the Model-View-Controller pattern. This is what the implementation of the UITableViewDataSource
protocol looks like in the SettingsViewController
class.
// MARK: - Table View Data Source Methods
func numberOfSections(in tableView: UITableView) -> Int {
return Section.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let section = Section(rawValue: section) else { fatalError("Unexpected Section") }
return section.numberOfRows
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let section = Section(rawValue: indexPath.section) else { fatalError("Unexpected Section") }
guard let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.reuseIdentifier, for: indexPath) as? SettingsTableViewCell else { fatalError("Unexpected Table View Cell") }
switch section {
case .time:
cell.mainLabel.text = (indexPath.row == 0) ? "12 Hour" : "24 Hour"
let timeNotation = UserDefaults.timeNotation()
if indexPath.row == timeNotation.rawValue {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
case .units:
cell.mainLabel.text = (indexPath.row == 0) ? "Imperial" : "Metric"
let unitsNotation = UserDefaults.unitsNotation()
if indexPath.row == unitsNotation.rawValue {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
case .temperature:
cell.mainLabel.text = (indexPath.row == 0) ? "Fahrenheit" : "Celcius"
let temperatureNotation = UserDefaults.temperatureNotation()
if indexPath.row == temperatureNotation.rawValue {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
}
return cell
}
Using the Model-View-ViewModel pattern, we can rid the settings view controller of the task to configure table view cells. That is not the responsibility of the view controller. This may seem unnecessary for a view controller as simple as this one, but imagine a view controller managing dozens of settings and configuration options. Samsara is a fine example of this.
Again, it is possible to put the view controller in charge of figuring out what each table view cell needs to display. But why would you make your life more difficult if a pattern like MVVM can elegantly solve this problem. The result is a lightweight, focused view controller. The view models are simple to implement, lightweight, and very easy to test.
Let me show you how the settings view controller can benefit from the Model-View-ViewModel pattern.
Creating a View Model for Each Section
There are several solutions to solve this problem. One solution is to create a view model for each section. Let us start with the Time Notation section. We create a new file, SettingsViewTimeViewModel.swift, and define a struct named SettingsViewTimeViewModel
.
What should the view model look like? Remember that the view model keeps a reference to a model.
We define a property, timeNotation
, of type TimeNotation
.
import UIKit
struct SettingsViewTimeViewModel {
// MARK: - Properties
let timeNotation: TimeNotation
}
TimeNotation
is an enum with two members.
enum TimeNotation: Int {
case twelveHour
case twentyFourHour
}
The interface of the SettingsViewTimeViewModel
struct is going to be short and simple. Remember that the view controller asks the view model for the values it needs to configure a table view cell in the Time Notation section. It needs a string for the mainLabel
label and it needs to know the value of the table view cell's accessoryType
property. This is what the interface of the view model should look like.
import UIKit
struct SettingsViewTimeViewModel: SettingsRepresentable {
// MARK: - Properties
let timeNotation: TimeNotation
// MARK: - Public Interface
var text: String {
...
}
var accessoryType: UITableViewCellAccessoryType {
...
}
}
The implementation of the computed properties is trivial as you can see below.
import UIKit
struct SettingsViewTimeViewModel: SettingsRepresentable {
// MARK: - Properties
let timeNotation: TimeNotation
// MARK: - Public Interface
var text: String {
switch timeNotation {
case .twelveHour: return "12 Hour"
case .twentyFourHour: return "24 Hour"
}
}
var accessoryType: UITableViewCellAccessoryType {
if UserDefaults.timeNotation() == timeNotation {
return .checkmark
} else {
return .none
}
}
}
The implementations of the view models for the Units and Temperature sections are very similar.
import UIKit
struct SettingsViewUnitsViewModel: SettingsRepresentable {
// MARK: - Properties
let unitsNotation: UnitsNotation
// MARK: - Public Interface
var text: String {
switch unitsNotation {
case .imperial: return "Imperial"
case .metric: return "Metric"
}
}
var accessoryType: UITableViewCellAccessoryType {
if UserDefaults.unitsNotation() == unitsNotation {
return .checkmark
} else {
return .none
}
}
}
import UIKit
struct SettingsViewTemperatureViewModel: SettingsRepresentable {
// MARK: - Properties
let temperatureNotation: TemperatureNotation
// MARK: - Public Interface
var text: String {
switch temperatureNotation {
case .fahrenheit: return "Fahrenheit"
case .celsius: return "Celsius"
}
}
var accessoryType: UITableViewCellAccessoryType {
if UserDefaults.temperatureNotation() == temperatureNotation {
return .checkmark
} else {
return .none
}
}
}
Refactoring the Settings View Controller
With the view models ready to use, we can refactor the tableView(_:cellForRowAt:)
method of the settings view controller.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let section = Section(rawValue: indexPath.section) else { fatalError("Unexpected Section") }
guard let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.reuseIdentifier, for: indexPath) as? SettingsTableViewCell else { fatalError("Unexpected Table View Cell") }
switch section {
case .time:
guard let timeNotation = TimeNotation(rawValue: indexPath.row) else { fatalError("Unexpected Index Path") }
// Initialize View Model
let viewModel = SettingsViewTimeViewModel(timeNotation: timeNotation)
// Configure Cell
cell.mainLabel.text = viewModel.text
cell.accessoryType = viewModel.accessoryType
case .units:
guard let unitsNotation = UnitsNotation(rawValue: indexPath.row) else { fatalError("Unexpected Index Path") }
// Initialize View Model
let viewModel = SettingsViewUnitsViewModel(unitsNotation: unitsNotation)
// Configure Cell
cell.mainLabel.text = viewModel.text
cell.accessoryType = viewModel.accessoryType
case .temperature:
guard let temperatureNotation = TemperatureNotation(rawValue: indexPath.row) else { fatalError("Unexpected Index Path") }
// Initialize View Model
let viewModel = SettingsViewTemperatureViewModel(temperatureNotation: temperatureNotation)
// Configure Cell
cell.mainLabel.text = viewModel.text
cell.accessoryType = viewModel.accessoryType
}
return cell
}
To understand what happens, we take the Time Notation section as an example. We create an instance of the TimeNotation
enum using the value of the indexPath
parameter.
case .time:
guard let timeNotation = TimeNotation(rawValue: indexPath.row) else { fatalError("Unexpected Index Path") }
// Initialize View Model
let viewModel = SettingsViewTimeViewModel(timeNotation: timeNotation)
// Configure Cell
cell.mainLabel.text = viewModel.text
cell.accessoryType = viewModel.accessoryType
The TimeNotation
instance is used to instantiate the view model, an instance of the SettingsViewTimeViewModel
struct. The view controller uses the view model to configure the table view cell. That looks much better.
This implementation of the Model-View-ViewModel pattern slightly deviates from what we discussed yesterday. The view models used by the settings view controller are short-lived objects because the view controller doesn't keep a reference to the view models. But that isn't a problem. The view models are structures, value types that are inexpensive to make. They are instantiated, used to configure a table view cell, and discarded soon thereafter.
But We Can Do Better
But we can do better than this. Notice that we repeat ourselves in the tableView(_:cellForRowAt:)
method. In the next tutorial, we use protocols to further simplify the settings view controller.
You can learn more about the Model-View-ViewModel pattern and Swift in Mastering Model-View-ViewModel With Swift. Check it out.