Earlier in this series, we broke the settings view. The root view controller acts as the delegate of the settings view controller, but nothing happens when the user updates a setting in the settings view. In this and the next episode, we fix what we broke and reactify the application's settings view.
Creating a View Model
The plan is to update the table view cells of the settings view controller using Combine. We first create a view model for the settings view controller. Add a Swift file to the View Models group and name it SettingsViewModel.swift. Add an import statement for the Combine framework and declare a final class with name SettingsViewModel.
import Combine
import Foundation
final class SettingsViewModel {
}
We declare a property for each setting, starting with the time notation setting. Declare a variable property with name timeNotation. The initial value of the property is the value that is stored in the user defaults database. We prefix the property with the Published property wrapper. We covered the Published property wrapper earlier in this series.
import Combine
import Foundation
final class SettingsViewModel {
// MARK: - Properties
@Published var timeNotation = UserDefaults.timeNotation
}
Even though the timeNotation property isn't declared privately, the settings view model also exposes a publisher that emits the time notation setting. This makes it easier to inject the publisher into other objects. Don't worry about this for now. It will make sense in the next episode.
Declare a computed property with name timeNotationPublisher. The publisher's Output type is TimeNotation and its Failure type is Never. In the body of the computed property, we wrap the publisher of the timeNotation property with a type eraser using the eraseToAnyPublisher() method and return the resulting publisher. By exposing a publisher of type AnyPublisher, it is easier to integrate the publishers of the settings view model into the rest of the project. This will make sense in the next episode.
import Combine
import Foundation
final class SettingsViewModel {
// MARK: - Properties
var timeNotationPublisher: AnyPublisher<TimeNotation, Never> {
$timeNotation
.eraseToAnyPublisher()
}
@Published var timeNotation = UserDefaults.timeNotation
}
We repeat these steps for the units and temperature notation settings.
import Combine
import Foundation
final class SettingsViewModel {
// MARK: - Properties
var timeNotationPublisher: AnyPublisher<TimeNotation, Never> {
$timeNotation
.eraseToAnyPublisher()
}
@Published var timeNotation = UserDefaults.timeNotation
// MARK: -
var unitsNotationPublisher: AnyPublisher<UnitsNotation, Never> {
$unitsNotation
.eraseToAnyPublisher()
}
@Published var unitsNotation = UserDefaults.unitsNotation
// MARK: -
var temperatureNotationPublisher: AnyPublisher<TemperatureNotation, Never> {
$temperatureNotation
.eraseToAnyPublisher()
}
@Published var temperatureNotation = UserDefaults.temperatureNotation
}
Every time the timeNotation, unitsNotation, or temperatureNotation properties are set, the user defaults database should be updated with the new value. While we could use a didSet property observer, remember that a property observer is often a code smell in a project that embraces Combine.
Declare a private, variable property, subscriptions, of type Set<AnyCancellable> and set its initial value to an empty set.
private var subscriptions: Set<AnyCancellable> = []
In the initializer of the SettingsViewModel class, we invoke a helper method, setupBindings(). In setupBindings(), the view model attaches itself as a subscriber to the publisher of the timeNotation property using the sink(_:) method. In the closure that is passed to the sink(_:) method, the view model updates the user defaults database with the new value. The subscription is added to the subscriptions property.
// MARK: - Initialization
init() {
// Setup Bindings
setupBindings()
}
// MARK: - Helper Methods
private func setupBindings() {
// Subscribe to Time Notation Publisher
$timeNotation
.sink {
UserDefaults.timeNotation = $0
}.store(in: &subscriptions)
}
We repeat these steps for the units and temperature notation settings.
// MARK: - Initialization
init() {
// Setup Bindings
setupBindings()
}
// MARK: - Helper Methods
private func setupBindings() {
// Subscribe to Time Notation Publisher
$timeNotation
.sink {
UserDefaults.timeNotation = $0
}.store(in: &subscriptions)
// Subscribe to Units Notation Publisher
$unitsNotation
.sink {
UserDefaults.unitsNotation = $0
}.store(in: &subscriptions)
// Subscribe to Temperature Notation Publisher
$temperatureNotation
.sink {
UserDefaults.temperatureNotation = $0
}.store(in: &subscriptions)
}
With the SettingsViewModel class in place, it is time to put it to use.
Injecting the View Model
Open SettingsViewController.swift and add an import statement for the Combine framework at the top.
import UIKit
import Combine
final class SettingsViewController: UIViewController {
...
}
Declare a private, constant property, viewModel, of type SettingsViewModel.
final class SettingsViewController: UIViewController {
// MARK: - Properties
private let viewModel: SettingsViewModel
...
}
Because we declare the viewModel property as a private, constant property, it needs to be set during initialization using initializer injection. Let's implement an initializer to meet that requirement. Define an initializer with name init(coder:viewModel:). The initializer accepts an NSCoder instance as its first argument and a SettingsViewModel instance as its second argument. In the initializer, we set the viewModel property and invoke the inherited init(coder:) initializer.
// MARK: - Initialization
init?(coder: NSCoder, viewModel: SettingsViewModel) {
self.viewModel = viewModel
super.init(coder: coder)
}
We are also required to implement the init(coder:) initializer. Because it shouldn't be used to instantiate a SettingsViewController instance, we throw a fatal error in the init(coder:) initializer.
required init?(coder: NSCoder) {
fatalError("Use `init(coder:viewModel:)` to initialize a `SettingsViewController` instance.")
}
The settings view controller is instantiated when a segue of the main storyboard is performed. We use a segue action to invoke the custom initializer of the SettingsViewController class to instantiate the settings view controller.
Open RootViewController.swift and define a private method with name showSettings(coder:). A segue action is nothing more than a method prefixed with the IBSegueAction attribute. By applying the IBSegueAction attribute to the method, we expose it as a segue action to Interface Builder.
The segue action accepts an argument of type NSCoder. Its return type is SettingsViewController?. In the body of the showSettings(coder:) method, we create a SettingsViewModel instance and pass it as the second argument of the custom initializer of the SettingsViewController class. The first argument of the initializer is the NSCoder instance.
// MARK: - Segue Actions
@IBSegueAction private func showSettings(coder: NSCoder) -> SettingsViewController? {
// Initialize Settings View Model
let viewModel = SettingsViewModel()
// Initialize Settings View Controller
return SettingsViewController(coder: coder, viewModel: viewModel)
}
Before we move on, we need to connect the segue action to the segue in Interface Builder. Open Main.storyboard and select the root view controller. Press Control and drag from the segue connecting the navigation controller and the settings view controller to the root view controller. Choose the showSettingsWithCoder segue action from the list of segue actions.

Select the segue and open the Connections Inspector on the right to make sure it is connected to the segue action of the root view controller.

Reactifying the Settings View
To reactify the settings view, we need to make two changes. First, we need to change how the view controller configures the cells of its table view. Second, we need to change the implementation of the tableView(_:didSelectRowAt:) method. The view controller should no longer notify its delegate. Let's start with the tableView(_:didSelectRowAt:) method.
The view controller no longer notifies its delegate. It simply updates the properties of its view model we declared earlier. Every time a property of the view model receives a new value, the publisher of the property emits the new value and the user defaults database is updated.
// MARK: - Table View Delegate Methods
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let section = Section(rawValue: indexPath.section) else {
fatalError("Unexpected Section")
}
switch section {
case .time:
if let timeNotation = TimeNotation(rawValue: indexPath.row) {
viewModel.timeNotation = timeNotation
}
case .units:
if let unitsNotation = UnitsNotation(rawValue: indexPath.row) {
viewModel.unitsNotation = unitsNotation
}
case .temperature:
if let temperatureNotation = TemperatureNotation(rawValue: indexPath.row) {
viewModel.temperatureNotation = temperatureNotation
}
}
}
To configure the cells of the table view, the settings view controller defines a helper method, settingsPresentable(for:), that returns a SettingsPresentable object. To reactify the settings view, the helper method no longer returns a SettingsPresentable object. Instead, it returns a publisher that emits SettingsPresentable objects. We first change the name of the method to settingsPresentablePublisher(for:).
private func settingsPresentablePublisher(for indexPath: IndexPath) -> AnyPublisher<SettingsPresentable, Never> {
...
}
We also need to update the view models that conform to the SettingsPresentable protocol, SettingsTimeViewModel, SettingsUnitsViewModel, and SettingsTemperatureViewModel. The view models should no longer ask the user defaults database for the current setting. Open SettingsTimeViewModel.swift and declare a property, selection, of type timeNotation. In the closure of the accessoryType computed property, the view model compares the value of timeNotation and selection to determine which value to return.
import UIKit
struct SettingsTimeViewModel: SettingsPresentable {
// MARK: - Properties
let timeNotation: TimeNotation
// MARK: -
let selection: TimeNotation
// MARK: - Public API
var text: String {
switch timeNotation {
case .twelveHour: return "12 Hour"
case .twentyFourHour: return "24 Hour"
}
}
var accessoryType: UITableViewCell.AccessoryType {
timeNotation == selection
? .checkmark
: .none
}
}
We repeat these steps for the SettingsUnitsViewModel and SettingsTemperatureViewModel structs.
import UIKit
struct SettingsUnitsViewModel: SettingsPresentable {
// MARK: - Properties
let unitsNotation: UnitsNotation
// MARK: -
let selection: UnitsNotation
// MARK: - Public API
var text: String {
switch unitsNotation {
case .imperial: return "Imperial"
case .metric: return "Metric"
}
}
var accessoryType: UITableViewCell.AccessoryType {
unitsNotation == selection
? .checkmark
: .none
}
}
import UIKit
struct SettingsTemperatureViewModel: SettingsPresentable {
// MARK: - Properties
let temperatureNotation: TemperatureNotation
// MARK: -
let selection: TemperatureNotation
// MARK: - Public Interface
var text: String {
switch temperatureNotation {
case .fahrenheit: return "Fahrenheit"
case .celsius: return "Celsius"
}
}
var accessoryType: UITableViewCell.AccessoryType {
temperatureNotation == selection
? .checkmark
: .none
}
}
Revisit SettingsViewController.swift and navigate to the settingsPresentablePublisher(for:) method. We return a publisher from each case of the switch statement. Let's start with the time case. We apply the compactMap operator to the timeNotationPublisher property of the view controller's view model. The closure of the compactMap operator accepts the time notation setting as its only argument and it returns an object of type SettingsPresentable?.
We use the index path that is passed to the settingsPresentablePublisher(for:) method to create a TimeNotation object. That is the TimeNotation object that corresponds with the row of the table view. We return nil if the creation of the TimeNotation object fails. That is why we apply the compactMap operator instead of the map operator. We use the TimeNotation objects to create a SettingsTimeViewModel object and return it from the closure. We wrap the resulting publisher with a type eraser using the eraseToAnyPublisher() method and return the resulting publisher.
private func settingsPresentablePublisher(for indexPath: IndexPath) -> AnyPublisher<SettingsPresentable, Never> {
guard let section = Section(rawValue: indexPath.section) else {
fatalError("Unexpected Section")
}
switch section {
case .time:
return viewModel.timeNotationPublisher
.compactMap { selection -> SettingsPresentable? in
guard let timeNotation = TimeNotation(rawValue: indexPath.row) else {
return nil
}
return SettingsTimeViewModel(timeNotation: timeNotation, selection: selection)
}
.eraseToAnyPublisher()
case .units:
...
case .temperature:
...
}
}
The implementation of the units and temperature cases are very similar.
private func settingsPresentablePublisher(for indexPath: IndexPath) -> AnyPublisher<SettingsPresentable, Never> {
guard let section = Section(rawValue: indexPath.section) else {
fatalError("Unexpected Section")
}
switch section {
case .time:
return viewModel.timeNotationPublisher
.compactMap { selection -> SettingsPresentable? in
guard let timeNotation = TimeNotation(rawValue: indexPath.row) else {
return nil
}
return SettingsTimeViewModel(timeNotation: timeNotation, selection: selection)
}
.eraseToAnyPublisher()
case .units:
return viewModel.unitsNotationPublisher
.compactMap { selection -> SettingsPresentable? in
guard let unitsNotation = UnitsNotation(rawValue: indexPath.row) else {
return nil
}
return SettingsUnitsViewModel(unitsNotation: unitsNotation, selection: selection)
}
.eraseToAnyPublisher()
case .temperature:
return viewModel.temperatureNotationPublisher
.compactMap { selection -> SettingsPresentable? in
guard let temperatureNotation = TemperatureNotation(rawValue: indexPath.row) else {
return nil
}
return SettingsTemperatureViewModel(temperatureNotation: temperatureNotation, selection: selection)
}
.eraseToAnyPublisher()
}
}
The last piece of the puzzle is putting the publisher the settingsPresentablePublisher(for:) method returns to use in the tableView(_:cellForRowAt:) method. We first need to reactify the SettingsTableViewCell class. Open SettingsTableViewCell.swift and declare a private, variable property, subscriptions, of type Set<AnyCancellable> and set its initial value to an empty set.
import UIKit
import Combine
final class SettingsTableViewCell: UITableViewCell {
// MARK: - Properties
@IBOutlet private var mainLabel: UILabel!
// MARK: -
private var subscriptions: Set<AnyCancellable> = []
...
}
Because a table view reuses its cells, we need to cancel the subscriptions that are stored in the subscriptions property before a cell is reused. We override the prepareForReuse() method and call removeAll() on the subscriptions property. Remember that an AnyCancellable instance calls its cancel() method when it is deinitialized. That is exactly what we want.
override func prepareForReuse() {
super.prepareForReuse()
// Cancel Subscriptions
subscriptions.removeAll()
}
Remove the configure(with:) method. We no longer need it. We define a method that accepts a publisher. The Output and Failure type of the publisher match those of the publisher returned by the settingsPresentablePublisher(for:) method.
func bind(to presentablePublisher: AnyPublisher<SettingsPresentable, Never>) {
}
The table view cell attaches itself as a subscriber to the publisher using the sink(_:) method. In the closure that is passed to the sink(_:) method, the table view cell updates its label and accessory type using the SettingsPresentable object. The subscription is stored in the subscriptions property.
// MARK: - Public API
func bind(to presentablePublisher: AnyPublisher<SettingsPresentable, Never>) {
// Subscribe to Presentable Publisher
presentablePublisher
.sink { [weak self] presentable in
self?.mainLabel.text = presentable.text
self?.accessoryType = presentable.accessoryType
}.store(in: &subscriptions)
}
Revisit SettingsViewController.swift and navigate to the tableView(_:cellForRowAt:) method. The view controller invokes the settingsPresentablePublisher(for:) method to create a publisher for the table view cell, passing the publisher to the table view cell's bind(to:) method.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.reuseIdentifier, for: indexPath) as? SettingsTableViewCell else {
fatalError("Unable to Dequeue Settings Table View Cell")
}
// Setup Bindings
cell.bind(to: settingsPresentablePublisher(for: indexPath))
return cell
}
Build and run the application to see the result. The behavior of the settings view hasn't changed. The settings view is still broken, though. We fix that in the next episode.
Cleaning Up the Settings View Controller
The settings view controller no longer notifies its delegate when a setting changes, which means we can remove the SettingsViewControllerDelegate protocol and the delegate property in SettingsViewController.swift. Open RootViewController.swift and remove the extension that conforms the RootViewController class to the SettingsViewControllerDelegate protocol.
Navigate to the prepare(for:sender:) method and remove the case in which the settings view controller is configured. That is no longer needed. We can also remove the settingsView static property from the Segue enum.
private enum Segue {
static let dayView = "SegueDayView"
static let weekView = "SegueWeekView"
static let locationsView = "SegueLocationsView"
}
What's Next?
In this episode, we reactified the table view cells of the settings view controller using Combine. We also replaced delegation with a reactive approach. The settings view controller no longer relies on delegation to notify the root view controller. The settings view is still broken, though. We fix that in the next episode.