Yesterday, you learned how to use view models in a view controller that isn't driven by data. We refactored the settings view controller of Cloudy.

But there is room for improvement. We are repeating ourselves in the tableView(_:cellForRowAt:) method of the UITableViewDataSource protocol. We can improve this using protocol-oriented programming.

Creating a Protocol

The first improvement I want to make is removing the duplicate code in the tableView(_:cellForRowAt:) method of the UITableViewDataSource protocol.

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
}

We can optimize the implementation of the tableView(_:cellForRowAt:) method with a protocol. Create a new file and name it SettingsRepresentable.swift. Add an import statement for the UIKit framework at the top and define the SettingsRepresentable protocol. The interface of the protocol should reflect that of the view models we created in the previous tutorial. This is what the protocol definition should look like.

import UIKit

protocol SettingsRepresentable {

    var text: String { get }
    var accessoryType: UITableViewCellAccessoryType { get }

}

Before we can use the protocol in the settings view controller, the view models we created earlier need to conform to the protocol. That is easy, though. We only need to add the protocol to the type definition of each view model.

import UIKit

struct SettingsViewTimeViewModel: SettingsRepresentable {
    ...
}
import UIKit

struct SettingsViewUnitsViewModel: SettingsRepresentable {
    ...
}
import UIKit

struct SettingsViewTemperatureViewModel: SettingsRepresentable {
    ...
}

That's it. Each of the view models automatically conforms to the SettingsRepresentable protocol.

Refactoring the Settings View Controller

We can now update the tableView(_:cellForRowAt:) method of the SettingsViewController class. We declare a variable, viewModel, of type SettingsRepresentable? and assign the view model to that variable. If viewModel has a value, the view controller uses the view model to configure the table view cell.

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

    var viewModel: SettingsRepresentable?

    switch section {
    case .time:
        guard let timeNotation = TimeNotation(rawValue: indexPath.row) else { fatalError("Unexpected Index Path") }
        viewModel = SettingsViewTimeViewModel(timeNotation: timeNotation)
    case .units:
        guard let unitsNotation = UnitsNotation(rawValue: indexPath.row) else { fatalError("Unexpected Index Path") }
        viewModel = SettingsViewUnitsViewModel(unitsNotation: unitsNotation)
    case .temperature:
        guard let temperatureNotation = TemperatureNotation(rawValue: indexPath.row) else { fatalError("Unexpected Index Path") }
        viewModel = SettingsViewTemperatureViewModel(temperatureNotation: temperatureNotation)
    }

    if let viewModel = viewModel {
        cell.mainLabel.text = viewModel.text
        cell.accessoryType = viewModel.accessoryType
    }

    return cell
}

Better but Not Perfect

While this is an improvement, I would like to further reduce the involvement of the settings view controller. The SettingsTableViewCell is perfectly capable of configuring itself if we hand it a view model. All the table view cell needs to do is ask the view model for the values it needs to configure itself.

Note that this is very different from passing a model to a view, which is certainly not what we intend to do. The table view cell won’t know about the model, it will use the interface of the view model we give it. Because the view model conforms to the SettingsRepresentable protocol, the SettingsTableViewCell doesn't even need to know about any of the view models. That is the flexibility of protocol-oriented programming.

To make this work, we need to update the SettingsTableViewCell class. Open SettingsTableViewCell.swift and define a new method, configure(withViewModel:), which accepts one parameter of type SettingsRepresentable.

// MARK: - Configuration

func configure(withViewModel viewModel: SettingsRepresentable) {
    mainLabel.text = viewModel.text
    accessoryType = viewModel.accessoryType
}

The implementation is straightforward. The table view cell asks the view model for the values it needs to populate and configure itself.

The final change we need to make is update the tableView(_:cellForRowAt:) of the SettingsViewController class. As you can see, the role of the view controller is very limited. It instantiates a view model for each table view cell and passes the view model to the configure(withViewModel:) method of the table view cell. The table view cell takes care of the rest.

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

    var viewModel: SettingsRepresentable?

    switch section {
    case .time:
        guard let timeNotation = TimeNotation(rawValue: indexPath.row) else { fatalError("Unexpected Index Path") }
        viewModel = SettingsViewTimeViewModel(timeNotation: timeNotation)
    case .units:
        guard let unitsNotation = UnitsNotation(rawValue: indexPath.row) else { fatalError("Unexpected Index Path") }
        viewModel = SettingsViewUnitsViewModel(unitsNotation: unitsNotation)
    case .temperature:
        guard let temperatureNotation = TemperatureNotation(rawValue: indexPath.row) else { fatalError("Unexpected Index Path") }
        viewModel = SettingsViewTemperatureViewModel(temperatureNotation: temperatureNotation)
    }

    if let viewModel = viewModel {
        cell.configure(withViewModel: viewModel)
    }

    return cell
}

What's Next?

The settings view controller is a simple view controller, but I hope you can see the potential of the Model-View-ViewModel pattern in terms of simplifying view controllers. The settings view controller is very focused. All it does is manage its view and subviews and handle user interaction. That is the primary role of every view controller. And this applies to Model-View-ViewModel as well as Model-View-Controller.

You can learn more about the Model-View-ViewModel pattern and Swift in Mastering Model-View-ViewModel With Swift. Check it out.