Expanding Swift through Protocols and Extension
Build the “missing" language feature for Enums
Recently, I took a fresh look at adding a small list of attributes to an iOS app. When coding, the first question is often, what data structure should be used?
Since code logic will be based on these attributes, an enum is an obvious choice. A common alternative would be to use an array — populated either in code, from a data file in the app bundle, or by a table in the database.
The array is not a bad solution, but misses the opportunity to use the expressiveness of Swift to prevent typos from becoming errors and repeats similar code to produce the String representation.
In this post, we expand the implementation of enum through the use of a protocol extension that allows a single definition to enable control logic with type checked constants and populate UIPickerView with string labels required for the User Interface.
Let's get started by defining an enum, with the list of possible values for direction in the app.
enum Direction {
case north, south, east, west
}
Happy 😃, I now can use .south
in an if statement. If I transpose letters (i.e., sotuh) as I often do, it is a typo caught early as a syntax error and not a bug to be discovered at demo time.
if direction == .south {
play("JimmyBuffet")
}
As you code, requirements often grow. We now find ourselves needing to process each of the items we have defined. Lists, when represented as arrays, have all kinds of great additional capabilities in Swift. For example, we can use the for in
syntax to march through each item in the list or many of the other built in features including: map, contains, reduce, find, filter and others.
When we try this with our enum, we get a syntax error.
for direction in Direction {
doSomethingDirectional(direction)
}
//error: type 'Direction.Type' does not conform to protocol 'Sequence'
So we can’t enumerate the cases of an enum?
Feels to me like that should be a part of the language. Where is the allValues
attribute that found with dictionary? After a bit of searching, I confirmed that you can’t do this in Swift. However, this will not stop our journey.
Existing class, struct, enum types can be enhanced through the use of extensions, giving additional power to the language. In particular, we can add an attribute to enums that conforms to the sequence protocol, removing the cause of the above error message.
What’s a Protocol?
A protocol in Swift is a way to define the required functionality that other types can adopt. It is used to create a relationship so that service providing code compliant to the protocol can plug into instances relying on these services, which expands the capability of existing code.
The UITableViewDataSource
is an example of a protocol familiar to iOS developers. The datasource is used by the TableView to expand the capabilities of existing code to display application specific data.
The error message above is describing such a requirement that additional code is required to meet the requirements of the sequential protocol so that language features, such as map and for-in, can be used with our enum.
In our implementation, we are specifying the cases()
function to return a sequence that we then use to construct the allValues
array.
public protocol EnumCollection: Hashable {
static func cases() -> AnySequence<Self>
static var allValues: [Self] { get }
var caseName: String { get }
}
What’s an Extension ?
Extensions in Swift add new functionally to an existing class, structure, enumeration, or protocol type. That is, we can add to the behavior of these types without modifying the original source code, like adding UITableViewDataSource
to display data without changing the underlying code of the UITableView
.
We use an extension to enhance enums to add an instance method that implements the required code and packages it as an iterator within a sequence, thus conforming to the sequence protocol.
We then use that method to create a convenience attribute allValues
, which returns an array of all of the cases of the enum.
public extension EnumCollection {
public static func cases() -> AnySequence<Self> {
return AnySequence { () -> AnyIterator<Self> in
var raw = 0
return AnyIterator {
let current: Self = withUnsafePointer(to: &raw) { $0.withMemoryRebound(to: self, capacity: 1) { $0.pointee } }
guard current.hashValue == raw else {
return nil
}
raw += 1
return current
}
}
}
public static var allValues: [Self] {
return Array(self.cases())
}
public var caseName: String {
return "\(self)"
}
}
Once we have the extension defined, we can use the power of map to generate the very list we need to drive a UIPickerView
. Simply by adding the name of our Protocol to the enum definition, the new code is now part of this enum.
enum Direction: EnumCollection {
case north, south, east, west
}
print(Direction.allValues.map { $0.caseName.capitalized })
// output: [“North”, “South”, “East”, “West”]
A new requirement of the data service we must integrate with is that it uses an integer number to represent the points of the compass. We need that mapping between direction and compass point.
We were able to add it to our enum without breaking the ability to provide the strings representation for the user interface.
enum Direction : Int, EnumCollection {
case north = 0
case south = 180
case east = 90
case west = 270
var description: String {
return "\(self.caseName.capitalized)(\(self.rawValue))"
}
}
print(Direction(rawValue: 180)?.description ?? "Bad Direction")
// output: South(180)
The implementation of the PickerView Delegate and DataSource as shown below do not require relative lists of strings to provide the displayable list of directions. An example project is on GitHub.
// UIPickerViewDelegate, UIPickerViewDataSource
let pickerList = Direction.allValues
func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return pickerList.count
}
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
if let pickerDidChangeDirection = pickerDidChangeDirection {
pickerDidChangeDirection(pickerList[row])
}
}
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return pickerList[row].caseName.capitalized
}
Summary
The line of code you don't write is the line of code you never have to debug.
-Steve Jobs
We used protocols and extensions to enhance enums to implement a feature to access the list of enum cases as a sequence. This unleashed the existing capabilities of Swift in a new context, allowing us to amplify our work and use the data structure that was otherwise our primary choice.
Thanks to Tibor Bödecs' post revealing the core of the solution presented here.
I hope you found this post helpful. Feel free to reach out to me on GitHub, Twitter, or read more of my posts here. Let's connect on LinkedIn.