The many faces of Enums

Tim Johnson
Swifty Tim
8 min readSep 27, 2017

--

It’s been a pretty hectic few weeks for me, and it’s nice to finally get back to a point where I can settle into a routine. From a bachelor party to a week with a horrible, horrible cold, to a nice vacation in the Hudson Valley, I’ve once again found myself in Brooklyn, writing code in front of a computer (as opposed to that other kind with paper and a pen).

So here we are!

Swift has come with many improvements over Objective-C (for those tried and true Objective-C developers, I am open to insult), from protocol oriented programming, to a more functional approach to programming, and even to a better API around generics. There are plenty of features in Swift that make it a pleasant development experience.

I personally find that enums in Swift are awesome. They have been one of my more frequently used language features, to the point where I may be a little obsessive about them, to a fault.

But I’m going to walk through a few cases in which I think enums can be very powerful, specifically

  1. Clarification of seemingly arbitrary values
  2. Encapsulation of state
  3. Error wrapping

Clarification of seemingly arbitrary values

Enums are perfect for taking what may seem like obscure, hard to remember values, and applying meaning to them. This is primarily because of the fact they are readable values. Words we use in every day language have lots of meaning, and we can easily transfer that meaning on top of somewhat confusing values.

Lets take a CAGradientLayer for example.

CAGradientLayer has two properties, startPoint and endPoint which are both instances of CGPoint.

Apple documents these as follows

The start and end points of the gradient when drawn into the layer's coordinate space. The start point corresponds to the first gradient stop, the end point...

It’s an unnecessarily long description of the two properties. Both points represent the x and y coordinates of where the gradient starts, ranging from 0 to 1. The default values of startPoint and endPoint are [0.5, 0] and [0.5, 1] respectively. It’s hard to visualize what this means, but this is a vertical gradient going from top to bottom.

If we wanted to build a horizontal gradient layer, we would need to do something like this.

func horizontalGradientLayer() -> CAGradientLayer {
let gradient = CAGradientLayer()
gradient.startPoint = CGPoint(x: 0, y: 0.5)
gradient.endPoint = CGPoint(x: 1, y: 0.5)
return gradient
}

Even just typing this out now I had to think through the different possibilities of what startPoint and endPoint would correctly define a horizontal gradient. This kind of thinking is prone to errors.

Using an enum to define a gradient direction can improve our visualization of what the gradient actually looks like.

enum GradientDirection {
case vertical
case horizontal
}

This isn’t the most functional enum definition, but it is already easier to understand what each GradientDirection does. To make this useful, we’ll need to define a startPoint and endPoint for each direction.

enum GradientDirection {
case vertical
case horizontal
var startPoint: CGPoint {
get {
switch self {
case .vertical:
return CGPoint(x: 0.5, y: 0)
case .horizontal:
return CGPoint(x: 0, y: 0.5)
}
}
}
var endPoint: CGPoint {
get {
switch self {
case .vertical:
return CGPoint(x: 0.5, y: 1)
case .horizontal:
return CGPoint(x: 1, y: 0.5)
}
}
}
}

Now, when I want to define a gradient layer, it’s much more readable and therefore understandable.

func horizontalGradientLayer() -> CAGradientLayer {
let gradient = CAGradientLayer()
gradient.startPoint = GradientDirection.horizontal.startPoint
gradient.endPoint = GradientDirection.horizontal.endPoint
return gradient
}

Using the GradientDirection enum, we have gone from the very abstract concept of points on a plane, to the much more concrete concept of a direction.

Even better, the next developer that comes in to debug the gradient layer code can very easily see what was trying to be accomplished, because the enum applies understandable meaning to the values it is assigning.

Encapsulation of state

Very similar to how enums can help to apply meaning and a deeper understanding to abstract values, enums can also help to clearly define state. This is true for pretty much anything, from a complicated implementation of AVPlayer, to simply loading an object into a view.

A well defined and readable enum to represent state can greatly reduce the complexity of state management, and reduce the possibility of bugs or regressions from future developers.

Lets look at a simple feed.

class FeedViewController: UIViewController {
var feed: Feed?
let collectionView: CollectionView
let feedId: String
.
.
.
func fetchFeed() {
guard self.loadingView == nil else {
return
}
self.displayLoadingView()
NetworkService.shared.fetch(self.feedId) { (feed, error) in
if error != nil {
self.displayError(error.localizedDescription)
} else {
self.feed = feed
}
self.removeLoadingView()
}
}
}

There’s a lot going on here.

  • Check to see if there is a loading view currently on screen
  • Telling the view controller to display a loading view before the network call
  • Checking the value of the network call once it returns
  • If there is an error, display the error
  • Otherwise, set self.feed to the feed
  • Finally, remove the loading view from the view controller

The number of things that can go wrong here is overwhelming, and if any more complicated logic had to be handled here, this can very quickly get out of control.

And once again, if a future developer were to run into this code when trying to deal with a bug, he / she may refactor it without realizing the side effects they may cause.

Using an enum to encapsulate the state of the FeedViewController while it loads the feed can greatly reduce the complexity.

Lets define this enum.

enum FeedState {
case loading(String)
case loaded(Feed)
case empty(Error?)
}

This is a very clearly defined, as well as understandable, state. It can either be loading a feedId, be loaded with a feed, or be empty, with or without some error. All of these are very easily articulated, and how they relate the to feed is quite intuitive.

As is intended, this simplifies the logic in fetchFeed.

func fetchFeed(feedId: String) {
switch self.state {
case .loading:
return
default: continue
}
self.state = .loading(feedId)
NetworkService.shared.fetch(feedId) { (feed, error)
state = feed != nil ? .loaded(feed) : .empty(error)
}
}

Now, instead of handling all the manipulation of views and performing different operations based on the response from the network, the view controller is simply updating its state property.

Order of operations, proper handling of errors, all of that can be thrown out the window. The intrinsic complexity of the feed fetching function has been reduced to the assignment of a single property.

It can be assumed that the manipulation of views and handling of error will happen upon the assignment of the statement, something like the following.

var feedState: FeedState {
didSet {
switch feedState {
case .loading:
// Display loading view
case .loaded(let feed)
// reload table view
case .empty(let error)
// Display error if it exists
}
}
}

What views are displayed and how they are displayed is very tightly coupled to the different state. This makes it much easier to follow, much easier to debug, and much easier to manipulate down the road.

Not only does encapsulating state with enums make the code less prone to bugs, but it helps to define exactly what is expected of the feed view controller. Reading the code, it’s very clear what this view can do, and how it will react to certain configurations, which makes for a very friendly codebase.

Error wrapping

There is a common pattern forming here, and it is that of readability. Readability gives code meaning, and meaning in turn makes code future proof. Future proof in that developers maintaining the codebase will have a greater understanding of how the code is supposed to work, and be able to more successfully maintain it.

With that in mind, errors can often be frustrating to work with. Frequently, when a network error occurs, or when something unexpected happens in the codebase, string are passed around willy-nilly until they get to a place they can be exposed to the user.

There is no rhyme or reason to these error strings, and other than the strings themselves (or maybe a code they are attached to), there is very little meaning associated with them.

What do you know, another place where enums can save the day!

Using enums to wrap errors throughout an application can greatly improve the readability and usability of errors.

Lets take a look at a simple network error, as an example.

func fetchFeed() {
NetworkService.shared.fetch(feedId) { (feed, error) in
if error != nil {
switch error.code {
case 500:
// Display custom error
case 404:
// Display custom error
}
}
}
}

Luckily, we do get a code from the error, which allows us to handle different errors slightly differently. This is nice in terms of flexibility, but these error codes don’t really mean much in the end. They really are just numbers. Sometimes it’s even possible to get the same error code for multiple different reasons.

As the application scales, if this is how all errors are being handled, then error codes will become overused and cumbersome, and begin to lose even what little meaning they had.

Defining an enum can mitigate these issues. Lets define an enum for trying to login, as this can often be a tricky for displaying the proper error to the user.

enum LoginError: Error {
case invalidPassword
case invalidUsername
case invalidCredentials
}

The login error defines three cases. The case where the password is invalid, where the username is invalid, or the password / username combination is invalid. These are all a subset of an unauthorized error, but passing around a base error and having to parse out the necessary information at the termination point can get ugly.

Instead, wrapping the error in a LoginError at the entry point can greatly simplify how the errors are handled at the termination point.

func login() {
NetworkService.shared.login(credentials) { (user, error) in
switch error {
case .invalidPassword:
// Make password field red
case .invalidUsername:
// Make username field red
case .invalidCredentials:
// Display retry prompt
}
}
}

There is a very clear coupling between the kind of error and the result of that error here. If the design were to change for how a specific error were to be handled, it would be very easy to step into the code, find the appropriate error, and adjust the flow of the code.

And because the conversion of the server error to the error type is handled at the network layer, any change on server error handling doesn’t disrupt the view layer at all. The error parsing is obscured.

Using enums to wrap errors is actually a pretty common practice, but I find that coming back to it and re-evaluating the benefits can reinforce good practice and help to bring to light other places where enums can be useful.=

There are many more reasons to use enums in our applications, but the few I covered today I’ve found to be most influential when I’m writing code.

As I’ve stated before, enums really boil down to one simple concept, and that is readability, and how it applies meaning to our codebases.

I hope this was a good read, and feel free to leave any feedback!

--

--