Categories
iOS Swift

Sharing UI code with protocol extension

In this blog post we are going to look into how to share UI code between view controllers using protocol extension.

Problem setup

Let’s say we have two view controllers: one showing a list of items and another one showing items in a collection view. In iOS development it is common to use UITableViewController and UICollectionViewController subclasses for implementing the functionality.

final class TableViewController: UITableViewController {}
final class CollectionViewController: UICollectionViewController {}
view raw Views.swift hosted with ❤ by GitHub

What if both views do not have any items, then it would be nice to show a label indicating there is no content to show instead of a blank view. How to share code with both view controllers? Although there are multiple ways, in this case, let’s go for protocol extension.

Protocols and protocol extensions

Protocol is a set of methods, properties, and other requirements for enabling a particular functionality. In Swift, it is possible to provide a default implementation for methods and properties in a protocol. Meaning, whenever an object conforms to a protocol, it will gain an implementation of that method automatically. Otherwise the object itself must implement the method or property. This difference is presented in two code snippets where in the first one Car and Truck conform to InteriorLighting protocol and implement the interiorLightColor property themselves. In the second example the protocol is extended to have a default implementation of the property. It should be noted that both Car and Truck can still override default implementation themselves if needed.

import UIKit
protocol InteriorLighting {
var interiorLightColor: UIColor { get }
}
struct Car: InteriorLighting {
var interiorLightColor: UIColor {
return .white
}
}
struct Truck: InteriorLighting {
var interiorLightColor: UIColor {
return .white
}
}
print(Car().interiorLightColor) // UIExtendedGrayColorSpace 1 1
print(Truck().interiorLightColor) // UIExtendedGrayColorSpace 1 1

import UIKit
protocol InteriorLighting {
var interiorLightColor: UIColor { get }
}
extension InteriorLighting {
var interiorLightColor: UIColor {
return .white
}
}
struct Car: InteriorLighting {}
struct Truck: InteriorLighting {}
print(Car().interiorLightColor) // UIExtendedGrayColorSpace 1 1
print(Truck().interiorLightColor) // UIExtendedGrayColorSpace 1 1

Adding banner view when there is no content

Let’s now go back to TableViewController and CollectionViewController and add a banner view for both of those view controllers with protocol extension. Firstly, it is needed to define the protocol. Let’s name it as EmptyViewBanner. It has two properties: one for defining the string what to show and the other one for providing a container view for the banner as the protocol itself does not know anything about the view controllers. This protocol has a setter what mutates the conforming object. Therefore, let’s restrict the protocol to class only for requiring the conforming type to have reference semantics.

protocol EmptyViewBanner: AnyObject {
var bannerContainerView: UIView { get }
var emptyViewText: String { get set }
}

Next step is to provide a default implementation for the emptyViewText property. Its implementation will add a label to view hierarchy for showing the text. As protocol can’t have stored properties, it needs to use some other way for keeping a reference to the label. Having a tag and looking up by it will suffice.

extension EmptyViewBanner {
var emptyViewText: String {
get {
guard let label = bannerContainerView.viewWithTag(10000) as? UILabel else { return "" }
return label.text ?? ""
}
set {
guard newValue.isEmpty == false else {
bannerContainerView.viewWithTag(10000)?.removeFromSuperview()
return
}
if bannerContainerView.viewWithTag(10000) == nil {
let label = UILabel()
label.numberOfLines = 0
label.tag = 10000
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
bannerContainerView.addSubview(label)
label.leadingAnchor.constraint(equalToSystemSpacingAfter: bannerContainerView.safeAreaLayoutGuide.leadingAnchor, multiplier: 1.0).isActive = true
bannerContainerView.safeAreaLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: label.trailingAnchor, multiplier: 1.0).isActive = true
label.centerYAnchor.constraint(equalTo: bannerContainerView.safeAreaLayoutGuide.centerYAnchor).isActive = true
}
guard let label = bannerContainerView.viewWithTag(10000) as? UILabel else { fatalError() }
label.text = newValue
}
}
}

Lastly, both view controllers need to conform to EmptyViewBanner protocol and provide container views for the banner.

extension CollectionViewController: EmptyViewBanner {
var bannerContainerView: UIView {
return collectionView
}
}
extension TableViewController: EmptyViewBanner {
var bannerContainerView: UIView {
return tableView
}
}

Now it is possible to set text to both of the view controllers and protocol extension will take care of managing the label.

let contactsViewController: CollectionViewController =
let historyViewController: TableViewController =
contactsViewController.emptyViewText = "There are not any contacts"
historyViewController.emptyViewText = "There is no history available"

EmptyBannerView

Summary

In this blog post we looked into how to use protocol extension in Swift for sharing UI code. How to add a label to view hierarchy for showing a banner and how to keep a reference to it without using stored property.
Thank you for reading.

Playground

ProtocolExtensionForAddingUIElements (GitHub)
Xcode 10.0, Swift 4.2

References

Protocols (Swift.org)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s