10 Adding Protocols to the Mix

After we implemented the Model-View-ViewModel pattern in the settings view controller, we noticed we were repeating ourselves in the tableView(_:cellForRowAt:) method. We can solve this problem with a pinch of 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. This means we can create a protocol with an interface identical to those of the view models. Let me show you what I mean.

Creating the Protocol

Create a new group in the Settings View Controller group and name it Protocols. I prefer to keep protocols that are very specific close to where they’re used. But that’s just a personal preference.

Creating the Protocols Group
Creating the Protocols Group

Create a Swift file and name it SettingsRepresentable.swift.

Creating SettingsRepresentable.swift
Creating SettingsRepresentable.swift

Replace the import statement for Foundation with an import statement for UIKit and declare the SettingsRepresentable protocol.

SettingsRepresentable.swift

1 import UIKit
2 
3 protocol SettingsRepresentable {
4 
5 }

The protocol declares two properties, text of type String and accessoryType of type UITableViewCellAccessoryType.

SettingsRepresentable.swift

1 import UIKit
2 
3 protocol SettingsRepresentable {
4 
5     var text: String { get }
6     var accessoryType: UITableViewCellAccessoryType { get }
7 
8 }

Conforming to the Protocol

The next step is surprisingly easy. Because the three view models implicitly conform to the SettingsRepresentable protocol, we only need to make their conformance to the protocol explicit. We use an extension to accomplish this.

SettingsViewTimeViewModel.swift

 1 import UIKit
 2 
 3 struct SettingsViewTimeViewModel {
 4 
 5     ...
 6 
 7 }
 8 
 9 extension SettingsViewTimeViewModel: SettingsRepresentable {
10 
11 }

SettingsViewUnitsViewModel.swift

 1 import UIKit
 2 
 3 struct SettingsViewUnitsViewModel {
 4 
 5     ...
 6 
 7 }
 8 
 9 extension SettingsViewUnitsViewModel: SettingsRepresentable {
10 
11 }

SettingsViewTemperatureViewModel.swift

 1 import UIKit
 2 
 3 struct SettingsViewTemperatureViewModel {
 4 
 5     ...
 6 
 7 }
 8 
 9 extension SettingsViewTemperatureViewModel: SettingsRepresentable {
10 
11 }

Are you wondering why the extension is empty? We only use the extension to make the conformance of the view models to the SettingsRepresentable protocol explicit. There’s no need to implement the text and accessoryType properties of the protocol since the view models already implement these properties. This is important to understand.

Refactoring the Settings View Controller

It’s 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 SettingsRepresentable?.

SettingsViewController.swift

 1 func tableView(_ tableView: UITableView, cellForRowAt indexPath: Ind\
 2 exPath) -> UITableViewCell {
 3     guard let section = Section(rawValue: indexPath.section) else { \
 4 fatalError("Unexpected Section") }
 5     guard let cell = tableView.dequeueReusableCell(withIdentifier: S\
 6 ettingsTableViewCell.reuseIdentifier, for: indexPath) as? SettingsTa\
 7 bleViewCell else { fatalError("Unexpected Table View Cell") }
 8 
 9     // Helpers
10     var viewModel: SettingsRepresentable?
11 
12     switch section {
13         ...
14     }
15 
16     return cell
17 }

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’s a first step in the right direction.

SettingsViewController.swift

 1 func tableView(_ tableView: UITableView, cellForRowAt indexPath: Ind\
 2 exPath) -> UITableViewCell {
 3     guard let section = Section(rawValue: indexPath.section) else { \
 4 fatalError("Unexpected Section") }
 5     guard let cell = tableView.dequeueReusableCell(withIdentifier: S\
 6 ettingsTableViewCell.reuseIdentifier, for: indexPath) as? SettingsTa\
 7 bleViewCell else { fatalError("Unexpected Table View Cell") }
 8 
 9     // Helpers
10     var viewModel: SettingsRepresentable?
11 
12     switch section {
13     case .time:
14         guard let timeNotation = TimeNotation(rawValue: indexPath.ro\
15 w) else { fatalError("Unexpected Index Path") }
16         viewModel = SettingsViewTimeViewModel(timeNotation: timeNota\
17 tion)
18     case .units:
19         guard let unitsNotation = UnitsNotation(rawValue: indexPath.\
20 row) else { fatalError("Unexpected Index Path") }
21         viewModel = SettingsViewUnitsViewModel(unitsNotation: unitsN\
22 otation)
23     case .temperature:
24         guard let temperatureNotation = TemperatureNotation(rawValue\
25 : indexPath.row) else { fatalError("Unexpected Index Path") }
26         viewModel = SettingsViewTemperatureViewModel(temperatureNota\
27 tion: temperatureNotation)
28     }
29 
30     if let viewModel = viewModel {
31         // Configure Cell
32         cell.mainLabel.text = viewModel.text
33         cell.accessoryType = viewModel.accessoryType
34     }
35 
36     return cell
37 }

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