Using protocols as composable extensions
Today we will talk about using Protocols as composable pieces for our ViewControllers. Protocols and Protocol Extensions are my second favorite Swift feature after Optionals. It helps us to create highly composable and reusable codebase without inheritance. For years we were using inheritance as a gold programming standard. But is it so good? Let’s take a look for simple BaseViewController which we used to have in every project.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import UIKit
class BaseViewController: UIViewController {
private let activityIndicator = UIActivityIndicatorView(style: .whiteLarge)
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(activityIndicator)
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor)
])
}
func presenActivity() {
activityIndicator.startAnimating()
}
func dismissActivity() {
activityIndicator.stopAnimating()
}
func present(_ error: Error) {
let alert = UIAlertController(title: error.localizedDescription, message: nil, preferredStyle: .alert)
alert.addAction(.init(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
}
It looks straightforward and usable because most of our ViewControllers need activity indicator while downloading data from the internet and error handling in case of something goes wrong during the data download. But we don’t stop with this, and we add more features to BaseViewController over a time. It starts bloating with a lot of general-purpose functions. Here we have at least two main problems:
- Our BaseViewController breaks the Single Responsibility Principle by implementing all these features in one place. Over time it will turn into Massive-View-Controller, which hard to understand and cover with tests.
- Every ViewController in our app inherit from BaseViewController to use all these features. In case of a bug in BaseViewController, we will have this bug in all ViewControllers in our app even if ViewController is not using buggy functionality from BaseViewController.
Protocols for the rescue.
Protocol Extensions feature was released with Swift 2.0 and bring real power to protocol types which announce new paradigm of programming: Protocol Oriented Programming. I recommend you to watch the talk from WWDC about Protocols and Protocol extensions.
Let’s go back to our topic. How can Protocols help us? Let’s start by declaring ActivityPresentable Protocol for presenting and dismissing an activity indicator.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
protocol ActivityPresentable {
func presentActivity()
func dismissActivity()
}
extension ActivityPresentable where Self: UIViewController {
func presentActivity() {
if let activityIndicator = findActivity() {
activityIndicator.startAnimating()
} else {
let activityIndicator = UIActivityIndicatorView(style: .whiteLarge)
activityIndicator.startAnimating()
view.addSubview(activityIndicator)
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor)
])
}
}
func dismissActivity() {
findActivity()?.stopAnimating()
}
func findActivity() -> UIActivityIndicatorView? {
return view.subviews.compactMap { $0 as? UIActivityIndicatorView }.first
}
}
We extracted presentActivity and dismissActivity methods into the particular protocol type. We add default implementation via protocol extension for cases where Type which adopt this protocol is ViewController. It gives us the opportunity of using ViewController methods and properties in our protocol extension.
Let’s do the same for error presenting logic.
1
2
3
4
5
6
7
8
9
10
11
protocol ErrorPresentable {
func present(_ error: Error)
}
extension ErrorPresentable where Self: UIViewController {
func present(_ error: Error) {
let alert = UIAlertController(title: error.localizedDescription, message: nil, preferredStyle: .alert)
alert.addAction(.init(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
}
Now we have two reusable protocol types which respect the Single Responsibility Principle. We can add them as the extension to any ViewController which need this functionality. The nice thing is that we are adding the only extension which needed in concrete ViewController and not inherits all the stuff from the BaseViewController. Here is the usage example of these protocols.
1
2
3
4
5
6
7
8
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
presentActivity()
}
}
extension ViewController: ActivityPresentable, ErrorPresentable {}
Another opportunity here is that we can easily ignore default implementation of the protocol to implement our customized ActivityIndicator for some of ViewControllers. Let’s take a look at the example.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CustomViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
presentActivity()
}
}
extension CustomViewController: ActivityPresentable {
func presentActivity() {
// Custom activity presenting logic
}
func dismissActivity() {
}
}
While adopting CustomViewController to ActivityPresentable protocol, we specify the custom implementation of presentActivity and dismissActivity methods.
Conclusion
As you can see, we can use protocols as simple extensions for our ViewController type. In the future posts, we will continue using protocols to build reusable parts of ViewController. We will touch associated type, and conditional conformance features to develop more generic data based extensions for ViewControllers.