Level Up Your Code With Swift Enums

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.

swift enum pull quote

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.

enum CamelidType {
   case Unknown
   case Alpaca
   case Camel
   case Llama
}

When you need to define a type of camelid, you use the type and one of its values.

CamelidType camelId = .Alpaca

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.

xcode adding packages

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.)

swift enum adding packages

Finally, click the second Add Package button. You’re ready to go!

Sample Application

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.

starting the app

If you tap one, the app adds some text above the buttons.

adding a text

Then when you tap a different button, the text changes.

changing the text

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.

import SwiftUI

class Camelid: ObservableObject {
   @Published var species: String = ""
}

var camelid: Camelid = Camelid()

@main
struct Swift_EnumsApp: App {
   var body: some Scene {
       WindowGroup {
           ContentView(camelid: camelid)
       }
   }
}

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.

import SwiftUI

struct ContentView: View {

   @ObservedObject var camelid: Camelid

   var body: some View {
       VStack {
           if (camelid.species == "") {
               Text("")
                   .fixedSize(horizontal: true, vertical: true)
                   .foregroundColor(.blue)
           } else if (camelid.species == "Alpaca") {
               Text("I see an \(camelid.species)")
                   .fixedSize(horizontal: true, vertical: true)
                   .foregroundColor(.blue)
           } else {
               Text("I see a \(camelid.species)")
                   .fixedSize(horizontal: true, vertical: true)
                   .foregroundColor(.blue)
           }
           Spacer()
               .frame(height: 50)
           Button("Llama")
           {
               self.setCamelid(species: "Llama")
           }
           .buttonStyle(PlainButtonStyle())
           .foregroundColor(.blue)
           Spacer()
               .frame(height: 50)
           Button("Alpaca")
           {
               self.setCamelid(species: "Alpaca")
           }
           .buttonStyle(PlainButtonStyle())
           .foregroundColor(.blue)
           Spacer()
               .frame(height: 50)
           Button("Camel")
           {
               self.setCamelid(species: "Camel")
           }
           .buttonStyle(PlainButtonStyle())
           .foregroundColor(.blue)
       }
   }  
   func setCamelid(species: String) {
       camelid.species = species
   }
}

struct ContentView_Previews: PreviewProvider {
   static var previews: some View {
       ContentView(camelid: camelid)
   }
}

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.

class Swift_EnumsTests: XCTestCase {

   var sut: ContentView!

   override func setUpWithError() throws {
       // Put setup code here. This method is called before the invocation of each test method in the class.
       try super.setUpWithError()
       sut = ContentView(camelid: Camelid()  
   }

   override func tearDownWithError() throws {
       // Put teardown code here. This method is called after the invocation of each test method in the class.
       sut = nil
       try super.tearDownWithError()        
   }

   func testLlama() throws {
       // Set a species
       sut.setCamelid(species: "Llama")

       // Snap object state
       assertSnapshot(matching: sut, as: .dump)

       // Snap screen contents, compare to saved copy    
       assertSnapshot(
           matching: sut,
           as: .image(
               layout: .device(config: .iPhoneSe),
               traits: .init(displayScale: 2)
           )
       )
   }

   func testAlpaca() throws {
       sut.setCamelid(species: "Alpaca")

       assertSnapshot(matching: sut, as: .dump)
       assertSnapshot(
           matching: sut,
           as: .image(
               layout: .device(config: .iPhoneSe),
               traits: .init(displayScale: 2)
           )
       )        
   }

   func testCamel() throws {
       sut.setCamelid(species: "Camel")

       assertSnapshot(matching: sut, as: .dump)
       assertSnapshot(
           matching: sut,
           as: .image(
               layout: .device(config: .iPhoneSe),
               traits: .init(displayScale: 2)
           )
       )  
   }
}

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.

enum CamelidType: String, CaseIterable {
   case Unknown = "Unknown"
   case Alpaca = "Alpaca"
   case Camel = "Camel"
   case Llama = "Llama"
}

class Camelid: ObservableObject {
   @Published var species: CamelidType = .Unknown
}

var camelid: Camelid = Camelid()


@main
struct Swift_EnumsApp: App {
   var body: some Scene {
       WindowGroup {
           ContentView(camelid: camelid)
       }
   }
}

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.

struct ContentView: View {

   @ObservedObject var camelid: Camelid

   var body: some View {
       VStack {
           switch (camelid.species) {
               case .Unknown:
                   Text("")
                       .foregroundColor(.blue)
               case .Alpaca:
                   Text("I see an \(camelid.species.rawValue)")
                       .foregroundColor(.blue)
               default:
                   Text("I see a \(camelid.species.rawValue)")
                       .foregroundColor(.blue)
           }

           let values: [String] = CamelidType.allCases.map{ $0.rawValue}

           ForEach(Array(values.enumerated()), id:\.1) { (n, camelidType) in

               if (camelidType != "Unknown") {
                   Spacer()
                       .frame(height: 50)
                   Button(camelidType) {
                       self.setCamelid(species: CamelidType(rawValue: camelidType)!)
                   }
                   .buttonStyle(PlainButtonStyle())
                   .foregroundColor(.blue)
               }

           }
       }
   }

   func setCamelid(species: CamelidType) {
       camelid.species = species
   }
}
swift enum pull quote

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.

enum CamelidType: String, CaseIterable {
   case Unknown = "Unknown"
   case Alpaca = "Alpaca"
   case Camel = "Camel"
   case Llama = "Llama"
   case Dromedary = "Dromedary"
}

Now run the application.

running 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!).

Subscribe to our newsletter today

Thank you for subscribing to our blog!

gradient