Level Up Your Code With Swift Enums
A Swift enum (short for enumeration) defines a new type that contains a set of related values. These values can be any other valid type, or you can let Swift define it for you. Either way, enumerations make your code easier to read and more type safe.
This post will look at how to refactor your code to take advantage of Swift enumerations. We’ll start with a simple iOS application that isn’t using any enumerations. Then we’ll refactor it to use them. We’ll examine how enumerations make the code easier to understand and maintain.
What’s a Swift Enum?
Enumerations are sets of similar values together. You define one with a Swift enum keyword and a set of values. Let’s create an enumeration for camelid species.
When you need to define a type of camelid, you use the type and one of its values.
The possible values for camelId are limited to the ones in CamelidType, making it more type safe.
You can also iterate through the values of an enum like a collection and set different raw values for the members. I’ll show you how that works below.
Setting Up Xcode
I used Xcode 13.1 for the screenshots in this post.
You can download the sample source codel from here. The before version is on the main branch, and the enumerations version is on AddEnums.
The unit tests rely on the swift-snapshot-testing library. Installation instructions are in the README, but here’s a quick overview.
Select Add Packages from the Xcode file menu. Paste the GitHub URL (https://github.com/pointfreeco/swift-snapshot-testing) into the search box at the upper right-hand side.
Click the Add Package button. In the Choose Package Products dialog box, change the target to Swift EnumsTests (or to the test targets in your project if you’re writing your own code.)
Finally, click the second Add Package button. You’re ready to go!
Let’s look at some code and how enumerations can make it easier to maintain, debug, and read. This app displays different text depending on which button you tap.
When the app starts, it presents you with three buttons.
If you tap one, the app adds some text above the buttons.
Then when you tap a different button, the text changes.
The initial version of this app doesn’t use Swift enum.
No Swift Enum
The main application class is in Swift_EnumsApp.swift.
It defines an ObservableObject named Camelid on line 3. This class holds a string with the camelid the user has seen.
Line 7 creates an instance of the object. Then when it creates its only view, it injects the Camelid instance into it.
An ObservableObject is a mechanism for connecting changes made by GUI elements to an application’s underlying model. For example, two Views might share an ObservableObject. We’re using it here as a property that different views would share in a more complex application.
Let’s take a look at how the view in ContentView.swift uses it.
At startup, camelid contains an empty string, so the if/else block on lines 9–21 sets the Text at the top of the VStack to an empty string too.
Then each time you tap one of the buttons on lines 24–43, they set the new value using setCamelid(). This triggers a view change, and the if/else block sets the Text to the corresponding value.
Testing Without Enum
Our tests use setCamelid() to simulate button taps. Then it uses the snapshot library to examine the ContentView and compare a snapshot of the app screen to a saved image.
Line 17 is where the first test method starts. It calls setCamelid() with “Llama,” then snapshots the application twice. The first snapshot is a text representation of the app. The second is a screenshot.
The first time you run these tests, they will fail because the screenshots are not committed to git. Screenshots on my system may differ from yours. See the library’s README for details.
This application uses a String to indicate which camelid the user saw. Each view has to understand each possible value for Camelid. If we add a new species, we introduce potential issues. We could put them in one of the files as constants, but it’s still error prone.
The app is also using Strings to decide how to change the display. For a small application like this, a string comparison doesn’t have a significant impact on performance. But Strings can be expensive when you use them this way.
Let’s look at the same app but with Swift Enums instead.
With Swift Enum
First, we need to update Swift_EnumsApp.
On line 2, we define the Swift enum. This looks similar to the enum above, but we’ve added Strings for raw values to give us a way to collate each species with a String value. We’re also adding the CaseIterable protocol. You’ll see why below.
Next, we change Camelid’s single member to a CamelidType. Instead of initializing it with a String, we use .Unknown, which is shorthand for CamelidType.Unknown.
On line 12, we create the CamelId and pass it into the view on startup, just as before.
So let’s take a look at the view. This is where we’ll see the benefits of an enum over a String.
On lines 8–17, the if/else block is replaced with a shorter and easier-to-scan switch block. Instead of comparing String values, we switch on the enum cases.
Then, starting on line 19, we use CamelidType to define our buttons. Since we defined it with the CaseIterable protocol, we can use the allCases member to get a list of cases and put their rawValues in a map. Then we use the map to create the buttons. We skip the .Unknown case since we don’t want a button for it.
Finally, we change setCamelid() so it will work for the unit tests. Instead of using a String to set a species, we have a finite set of type-checked values.
When you run this new version of the application, it should work identically to the other.
Testing With Enum
The only change to the test code is to the setCamelid() calls. Change it from a String to the corresponding enum value.
That’s because of how the snapshot API works. We can’t iterate over the CamelidType enum as we did in the ContentView. It doesn’t know how to create different snapshots for each loop iteration inside the same test case.
Snapshot testing is great, but it’s still limited. In a more complex application, you could develop more unit tests or use Xcode UI tests to read alerts or accessibility values. But sometimes writing tests feels like the tail is wagging the dog.
That’s when no-code testing tools come to the rescue. Waldo has tools for recording and writing automated tests in your browser without a single line of code.
Adding a New Camelid With Enum
Let’s add a dromedary to the application.
Add a new case to the Camelid enum.
Now run the application.
“Dromedary” is there, and the button works!
Level Up With Swift Enums
We created a simple iOS application that changed its view based on which button you tapped. The initial version used String comparisons to determine how to react. We refactored it to use Swift enum to respond to the buttons and create them too.
Get started leveling up your code with Swift enums and Waldo tests today!
This post was written by Eric Goebelbecker. Eric has worked in the financial markets in New York City for 25 years, developing infrastructure for market data and financial information exchange (FIX) protocol networks. He loves to talk about what makes teams effective (or not so effective!).