The Model-View-ViewModel pattern isn't only useful for populating data-driven user interfaces. In this episode, I show you how to apply the MVVM pattern in the SettingsViewController class.
Remember that we want to extract logic from the controller that doesn't belong there. In this episode, we target the tableView(_:cellForRowAt:) method of the SettingsViewController class. The controller shouldn't need to figure out what it should display to the user. The plan is to create three view models the controller uses to populate its table view.
This example also illustrates that view models are sometimes short-lived objects. The controller doesn't necessarily need to keep a reference to the view model. That is something we haven't covered yet in this series.
Creating the Settings View Time View Model
We start by creating a group for the view models of the SettingsViewController class.

The first view model we create is a view model for populating the table view cells of the first section of the table view, the time section. This section is used for switching between 12 hour and 24 hour time notation. We create a new Swift file and name it SettingsTimeViewModel.swift.

We replace the import statement for Foundation with an import statement for UIKit and define the SettingsTimeViewModel struct.
SettingsTimeViewModel.swift
import UIKit
struct SettingsTimeViewModel {
}
The model of the struct should be of type TimeNotation. We define a new property, timeNotation, of type TimeNotation.
SettingsTimeViewModel.swift
import UIKit
struct SettingsTimeViewModel {
// MARK: - Properties
let timeNotation: TimeNotation
}
If we revisit the implementation of the tableView(_:cellForRowAt:) method in the SettingsViewController class, we can see which values the view model exposes to the controller. The settings view controller needs a value for the main label of the table view cell and it also needs to set the accessoryType property of the table view cell.
SettingsViewController.swift
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("Unable to Dequeue Settings 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:
...
case .temperature:
...
}
return cell
}
We need to implement two computed properties. The first computed property is named text and it is of type String. We use a simple switch statement to return a string based on the value of the timeNotation property. This should look familiar by now.
SettingsTimeViewModel.swift
var text: String {
switch timeNotation {
case .twelveHour: return "12 Hour"
case .twentyFourHour: return "24 Hour"
}
}
We create another computed property, accessoryType, of type UITableViewCell.AccessoryType. We fetch the user's preference from the user defaults database and compare it with the value of the timeNotation property. The result of this comparison defines the accessory type the view model returns.
SettingsTimeViewModel.swift
var accessoryType: UITableViewCell.AccessoryType {
if UserDefaults.timeNotation == timeNotation {
return .checkmark
} else {
return .none
}
}
That's it. The implementation of the SettingsTimeViewModel struct is short and simple.
Importing UIKit
Earlier in this series, I mentioned that importing the UIKit framework in a view model is a code smell. It is not necessarily wrong, but you need to be careful.
What we are doing in the SettingsTimeViewModel struct is open for discussion. Some developers might argue that this is wrong because we reference a trait of a view, a table view cell. I can understand and appreciate that argument.
No matter how you feel about the implementation of the SettingsTimeViewModel struct, what is most important is that you are aware of what we are doing. I have a clear philosophy about this subject. It is fine to break a rule as long as you know what the rule stands for and you understand the consequences.
That is enough philosophy for now. It is time to put the SettingsTimeViewModel struct to work in the SettingsViewController class.
Refactoring the Settings View Controller
Open SettingsViewController.swift and navigate to the tableView(_:cellForRowAt:) method. In the switch statement, we create a TimeNotation object using the value of the indexPath argument.
SettingsViewController.swift
case .time:
guard let timeNotation = TimeNotation(rawValue: indexPath.row) else {
fatalError("Unexpected Index Path")
}
The settings view controller uses the timeNotation object to create the view model.
SettingsViewController.swift
// Initialize View Model
let viewModel = SettingsTimeViewModel(timeNotation: timeNotation)
We use the view model to configure the table view cell.
SettingsViewController.swift
// Configure Cell
cell.mainLabel.text = viewModel.text
cell.accessoryType = viewModel.accessoryType
Your Turn
You should now be able to create the view models for the remaining two sections. Give it a try.
This is what the SettingsUnitsViewModel struct should look like. The most important difference with the SettingsTimeViewModel struct is the name and type of the model the view model manages.
SettingsUnitsViewModel.swift
import UIKit
struct SettingsUnitsViewModel {
// MARK: - Properties
let unitsNotation: UnitsNotation
// MARK: - Public API
var text: String {
switch unitsNotation {
case .imperial: return "Imperial"
case .metric: return "Metric"
}
}
var accessoryType: UITableViewCell.AccessoryType {
if UserDefaults.unitsNotation == unitsNotation {
return .checkmark
} else {
return .none
}
}
}
As you can see, the implementation of the SettingsTemperatureViewModel struct looks very similar.
SettingsTemperatureViewModel.swift
import UIKit
struct SettingsTemperatureViewModel {
// MARK: - Properties
let temperatureNotation: TemperatureNotation
// MARK: - Public Interface
var text: String {
switch temperatureNotation {
case .fahrenheit: return "Fahrenheit"
case .celsius: return "Celsius"
}
}
var accessoryType: UITableViewCell.AccessoryType {
if UserDefaults.temperatureNotation == temperatureNotation {
return .checkmark
} else {
return .none
}
}
}
The implementation of the tableView(_:cellForRowAt:) method isn't difficult to update either. In the units section, we create a UnitsNotation object, use it to create a view model, and configure the table view cell.
SettingsViewController.swift
case .units:
guard let unitsNotation = UnitsNotation(rawValue: indexPath.row) else {
fatalError("Unexpected Index Path")
}
// Initialize View Model
let viewModel = SettingsUnitsViewModel(unitsNotation: unitsNotation)
// Configure Cell
cell.mainLabel.text = viewModel.text
cell.accessoryType = viewModel.accessoryType
In the temperature section, we create a TemperatureNotation object, use it to create the view model, and configure the table view cell.
SettingsViewController.swift
case .temperature:
guard let temperatureNotation = TemperatureNotation(rawValue: indexPath.row) else { fatalError("Unexpected Index Path") }
// Initialize View Model
let viewModel = SettingsTemperatureViewModel(temperatureNotation: temperatureNotation)
// Configure Cell
cell.mainLabel.text = viewModel.text
cell.accessoryType = viewModel.accessoryType
Even though we simplified the tableView(_:cellForRowAt:) method of the SettingsViewController class, there is room for improvement. You may have noticed that we have duplications in the tableView(_:cellForRowAt:) method. We resolve these issues later in the series using protocol-oriented programming.
What's Next?
I am sure you agree that the view models we created are lightweight objects. Creating and discarding them doesn't impact performance because they are inexpensive to create.
Have you noticed that the view models are short-lived objects? The settings view controller doesn't keep a reference to the view models. The view models are created in the tableView(_:cellForRowAt:) method of the settings view controller and discarded soon after the method returns a table view cell. That is fine, though. It is a lightweight flavor of the Model-View-ViewModel pattern.