Use Enums Effectively
By: Riccardo Cipolleschi
Tags: Mobile, Swift Programming, Software Development, iOS, Apple

PROGRAMMING

Use Enums Effectively

How to properly use enums to model your data

Swift is an interesting language. It is very friendly for new developers: its syntax is concise and simple to grasp, as well as its main concepts.

However, it takes a lot of time to really master it. Every feature of the language has some details that are deeper than what described in swift.org’s Language Guide.

In today’s article, I’d like to explore some more advanced usages of enum s. Enum s are extremely powerful and they are often underused or misused. I hope to shed some light on ways to use them that you may have not thought about.

Models Exclusivity

Starting with the simplest idea, enum can be used to express alternatives: you need to choose between running , cycling and swimming ? Use an enum ! You need to list the days of the week? Use an enum !

These are the simplest cases. However, Swift’s enums are more powerful than that. Enum s support associated values , that are data related to a specific instance. For example, the running case can include the number of steps performed, the cycling case can include the gear used during the workout and the swimming session could be influenced by the water temperature.

Putting everything together, we end up with the following enum .

There are many other cases where the distinction is not that simple to abstract. In some cases, we would like to model real-life data that could or could not have some properties based on a classification of some type.

Let’s take bodyweight exercises, for example. There are squats, push-ups, planks, and iron chair. A workout is composed of different series of exercises. Someone could be tempted to model a series with a struct in the following way:

But there are some properties that do not really make sense. or instance, repetitionCount makes no sense for a plank series. That’s why we are forced to mark it as optional. At the same time, the duration may or may not make sense for the squats: if we are doing a circuit, we could want to do a finite number of repetitions; but if we are working out with a Tabata, we may want to do as much as squats as we can in a finite amount of time.

Therefore, we can refactor our model in the following way, which makes much more sense:

Tip: bypass the type for shared information

Notice how all the cases have the same exerciseId field, to connect the series with a specific exercise. While developing the app, we could incur in several places where we are not interested in the specific type of exercise, but we could be interested in generic information about it. We can create some computed variables that bypass the type expressed in the enum :

This approach is extremely useful to group the extraction of common properties within the type definition, so its calling site is kept clean and readable.

An Example From Apple: Result type in Foundation

Apple is subtly introducing this way of reasoning in its APIs as well. The first API provided by Apple to hit a server required a callback with (Data?, URLResponse?, Error?) parameters.

If the network call was successful, the Data type was populated while the Error was not. Viceversa, in case of a failure, the Error type was populated. Although the documentation states that one of the two cases is never nil , we could have both the fields set to nil , in principle: imagine a backend that responds with a 201 — No Content . The call succeeded but with no data nor errors!

To prevent this from happening, and to model directly in the code that either the data or the error is not nil, Apple started using a new data type called Result . Under the hood, Result is just an enum that can be in two states: success or failure . In case of a success , we may have some associated data with the content of the request — the old data . In the case of a failure , the associated value is the error with all the relevant information.

Polymorphism

Enums can be used to bypass the limitation of the homogeneous type in the collections. In Swift, a collection can only contain elements of the same type: if we define a Set<Int> we can store only integers in it. Let’s consider a photo editing app, where a user could add some text on top of a picture, as well as some stickers.

Text and Stickers have some very different behaviors and features: we could be tempted to model them with different structs.

This is not completely wrong: in the end, we need the fileURL and the text content to render them on top of the picture. However, we may want to store these attachments together, maybe to iterate over them or to store them in a layer. We can’t define an array: [Text | Sticker] in Swift, so what can we do?

As you may guess, we can use an enum . The enum will have a couple of cases ( .text and .sticker ) with their own associated values — the same structures we defined above. And now, yes! We can define an array: [Attachments] containing all the overlays:

Notice how we can bypass the enum ’s type to obtain the position of the attachment, as we described in the previous section.

OptionSet and EnumSet

Objective-C used to have enums as well. However, they were closer to C enums and not the powerful tool offered by Swift. They were always mapped to Integers . They were often used to implement a special data type that is called OptionSet . Everybody has used this kind of type at least once in their life: it is used to specify possible options to configure how some APIs should behave. A very common example is the UIView.animate API with its UIViewAnimationOptions option set.

There is something that bothers me with the standard OptionSet implementation. Let’s have a look at it, from the Apple official documentation:

There is nothing wrong with it, technically. However, it is not safe. To init this OptionSet , we need to pass an integer. The bitwise notation tells us that the 4 possible values are:

1 << 0 = (shift a 1 bit by 0 position to the left) = 1

= (shift a 1 bit by 0 position to the left) = 1 << 1 = (shift a 1 bit by 1position to the left) = 10 = 2

= (shift a 1 bit by 1position to the left) = = 1 << 2 = (shift a 1 bit by 2 positions to the left) = 100 = 4

= (shift a 1 bit by 2 positions to the left) = = 1 << 3 = (shift a 1 bit by 3positions to the left) = 1000 = 8

These values can sum together to obtain mixed configurations. The express value, for example, is the sum between .nextDay and .secondDay , therefore the actual value is 3 . As you can see, there is no other way to obtain a value of 3 , with the static values we defined. The following equation holds:

let express1 = ShippingOperation(rawValue: 3)

express1 == ShippingOperation.express // returns true

The maximum value we can reach is therefore 8 + 4 + 2 + 1 = 15 . So.. what happens if we write:

let unknownShipping = ShippingOperation(rawValue: 16)

// ??

Of course, it builds. It will create a ShippingOperation with that value but it has no meaning for our app. I think we can leverage enums to achieve a very similar result, safer!

Using Enums for OptionSet

What is an OptionSet , for real? It goes with its name: it is a set of options. Therefore, we can not have repeated elements — it’s a set, after all— and the Set algebra should work. So… what about using an enum , to define the alternatives, and a Set , to obtain uniqueness and the set algebra?

Et voila! We just replaced the OptionSet with these two smaller types. We can achieve the exact same functionalities of the OptionSet but in a safer way: we can no longer create not existing values and we can no longer “sum” operations, which in the real world does not make any sense.

Note: if you want, we can leverage the operators to achieve a nicer syntax. For example, we can introduce a + operator to simplify how to use these types:

Readability: Clean Code Without Nested Switches

While using enum consistently, it can happen pretty soon to have an enum whose associated value is an enum . Something like this:

This model is correct. However, if we want to perform some specific operations when our pet is a Siberian Husky, we end up writing code that looks like this:

Although this example is simple and not hard to read, in a real-world code this can get pretty messy pretty soon. There are two alternative ways to solve this conundrum:

explicitly unwrapping inner cases in the main switch

hiding the inner switch in a property.

The first technique works by expressing the alternative in the main switch ’s case clause. The switch ends up having more case clauses, but the indentation stays plainer and the code is easier to read.

The hiding technique is more suited when the logic in the inner switch is complex. We can create a property or a function that can handle all the cases of the inner switch . Then, we invoke it in the main switch . The benefit of this approach are:

an easier code to read, despite having to jump from a point to another of the code

the possibility to test the two functions/properties separately.

Conclusion

In today’s article, we explored enums and how they can be used in a better and more efficient way in our apps. We figured out how to write more expressive, readable, and safer code.

I hope that this will help everyone in writing cleaner and more readable code, to level up their software engineering skills!