In the previous episode, I mentioned that we are repeating ourselves in the tableView(_:cellForRowAt:) method of the SettingsViewController class. We can resolve this problem with protocol-oriented programming.

The idea is simple. The view models for each of the sections of the table view are very similar and the computed properties the view controller accesses have the same name. We can create a protocol with an interface identical to those of the view models. Let me show you what that looks like.

Creating the Protocol

Create a group in the Settings View Controller group and name it Protocols. I prefer to keep protocols close to where they are used. That is just a personal preference, though.

Creating the Protocols Group

Create a Swift file and name it SettingsPresentable.swift.

Creating SettingsPresentable.swift

Replace the import statement for Foundation with an import statement for UIKit and define the SettingsPresentable protocol.

SettingsPresentable.swift

import UIKit

protocol SettingsPresentable {

}

The protocol defines two properties, text of type String and accessoryType of type UITableViewCell.AccessoryType.

SettingsPresentable.swift

import UIKit

protocol SettingsPresentable {

    // MARK: - Properties

    var text: String { get }

    // MARK: -

    var accessoryType: UITableViewCell.AccessoryType { get }

}

Conforming to the Protocol

The next step is surprisingly simple. Because the three view models implicitly conform to the SettingsPresentable protocol, we only need to make their conformance to the protocol explicit. We use an extension to take care of that step.

Open SettingsTimeViewModel.swift and define an extension at the bottom. We use the extension to conform the SettingsTimeViewModel struct to the SettingsPresentable protocol.

SettingsTimeViewModel.swift

import UIKit

struct SettingsTimeViewModel {

    ...

}

extension SettingsTimeViewModel: SettingsPresentable {

}

We repeat these steps for the SettingsUnitsViewModel and SettingsTemperatureViewModel structs.

SettingsUnitsViewModel.swift

import UIKit

struct SettingsUnitsViewModel {

    ...

}

extension SettingsUnitsViewModel: SettingsPresentable {

}

SettingsTemperatureViewModel.swift

import UIKit

struct SettingsTemperatureViewModel {

    ...

}

extension SettingsTemperatureViewModel: SettingsPresentable {

}

We only use the extension to make the conformance of the view models to the SettingsPresentable protocol explicit. There is no need to implement the text and accessoryType properties of the SettingsPresentable protocol because the view models already implement these properties. That is important to understand.

Refactoring the Settings View Controller

It is time to update the SettingsViewController class. Open SettingsViewController.swift and navigate to the tableView(_:cellForRowAt:) method. Before entering the switch statement, we declare a variable, viewModel, of type SettingsPresentable?.

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")
    }

    // Helpers
    var viewModel: SettingsPresentable?

    switch section {
        ...
    }

    return cell
}

We set the value of this variable in the switch statement. We can remove the configuration of the table view cell from the switch statement and move it to the bottom of the implementation of the tableView(_:cellForRowAt:) method. That is a step in the right direction to avoid code duplication.

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")
    }

    // Helpers
    var viewModel: SettingsPresentable?

    switch section {
    case .time:
        guard let timeNotation = TimeNotation(rawValue: indexPath.row) else {
            fatalError("Unexpected Index Path")
        }

        // Initialize View Model
        viewModel = SettingsTimeViewModel(timeNotation: timeNotation)
    case .units:
        guard let unitsNotation = UnitsNotation(rawValue: indexPath.row) else {
            fatalError("Unexpected Index Path")
        }

        // Initialize View Model
        viewModel = SettingsUnitsViewModel(unitsNotation: unitsNotation)
    case .temperature:
        guard let temperatureNotation = TemperatureNotation(rawValue: indexPath.row) else { fatalError("Unexpected Index Path") }

        // Initialize View Model
        viewModel = SettingsTemperatureViewModel(temperatureNotation: temperatureNotation)
    }

    if let viewModel = viewModel {
        // Configure Cell
        cell.mainLabel.text = viewModel.text
        cell.accessoryType = viewModel.accessoryType
    }

    return cell
}

There is one subtle improvement we can make. We don't need to declare viewModel as an optional. We can declare it as a constant of type SettingsPresentable. We use a self-executing closure and move the switch statement into the closure. In each case, we return the view model instead of modifying the value of viewModel.

// Helpers
let viewModel: SettingsPresentable = {
    switch section {
    case .time:
        guard let timeNotation = TimeNotation(rawValue: indexPath.row) else {
            fatalError("Unexpected Index Path")
        }

        // Initialize View Model
        return SettingsTimeViewModel(timeNotation: timeNotation)
    case .units:
        guard let unitsNotation = UnitsNotation(rawValue: indexPath.row) else {
            fatalError("Unexpected Index Path")
        }

        // Initialize View Model
        return SettingsUnitsViewModel(unitsNotation: unitsNotation)
    case .temperature:
        guard let temperatureNotation = TemperatureNotation(rawValue: indexPath.row) else { fatalError("Unexpected Index Path") }

        // Initialize View Model
        return SettingsTemperatureViewModel(temperatureNotation: temperatureNotation)
    }
}()

This also means that we don't need to safely unwrap viewModel. I explain this pattern in more detail in What Are Self-Executing Closures.

// Configure Cell
cell.mainLabel.text = viewModel.text
cell.accessoryType = viewModel.accessoryType

What's Next?

Protocols work very well with the Model-View-ViewModel pattern. In the next episode, we take it one step further.