Write Better Code Using Swift Enums: A Detailed Guide

bugfenderapp

Bugfender

Posted on May 27, 2024

Write Better Code Using Swift Enums: A Detailed Guide

In Swift, an enum (short for enumeration) is a powerful feature that allows us to define a data type with a fixed set of related values so we can work with those values in a type-safe way within our code. In this article we’ll be taking a closer look at Swift enums and their applications in Swift, as well as providing some real-world examples of how we could deploy them in our builds.

We’ll be covering:

  • Creating and using Swift enums
  • Enums raw values
  • Enums associated values
  • Enums with raw and associated values
  • Swift Enum methods
  • Real-world examples of enum usage

We hope by the end of the article you’ll understand enums and how they’re used, and feel confident to give them a try in your own projects. Right, let’s get going…

Creating and using Swift enums

Creating a simple Swift enum is pretty straightforward, we simply use the enum keyword, for example:

enum DayOfTheWeek {
    case monday, tuesday, wednesday, thursday, friday, saturday, sunday
}

Enter fullscreen mode Exit fullscreen mode

In this example:

  • DayOfTheWeek – is the name of the enum
  • monday/tuesday/wednesday/thursday/friday/saturday/sunday– are the values defined within the enum

It’s worth pointing out here that with enums, ‘values’ are also referred to as ‘cases’, which is why we’ve used the case keyword when declaring the values inside our enum.

Now we’ve created our enum, we can use . notation to attribute values to any variable and Swift is smart enough to infer exactly what we’re trying to do:

var aDay: DayOfTheWeek

...

aDay = .thursday

Enter fullscreen mode Exit fullscreen mode

Another advantage is, since they’re constant values, we can easily use a Switch statement to transfer control.

For example, we could set up a method that prints to the console the day of the week, depending on which DayOfTheWeek it receives, like this:

func print(day: DayOfTheWeek) {
    switch day {
    case .monday: print("Monday")
    case .tuesday: print("Tuesday")
    case .wednesday: print("Wednesday")
    case .thursday: print("Thursday")
    case .friday: print("Friday")
    case .saturday: print("Saturday")
    case .sunday: print("Sunday")
    }
}

Enter fullscreen mode Exit fullscreen mode

And that’s the basics, good job. Now let’s build on that a little further.

Swift enums raw values

We would add a rawValue to an enum to associate it with a certain type, allowing us to use the enum cases as values of that type. To better understand this, let’s look at our example again:

enum DayOfTheWeek: Int {
    case monday = 1
    case tuesday = 2
    case wednesday = 3
    case thursday = 4 
    case friday = 5
    case saturday = 6
    case sunday = 7
}

Enter fullscreen mode Exit fullscreen mode

Here we’ve defined our enum as Int (this is so its cases can only include integer raw values) and assigned numerical values to each of our cases (days of the week) – these values are called raw values.

Now, if we queried DayOfTheWeek.wednesday.rawValue, we would get 3, as that’s the raw value associated with Wednesday.

Automatic inference

While in our example we assigned each case in our enum with a raw value individually, Swift is intuitive and can infer raw values for all cases if just one is assigned.

For example, we could have assigned numerical raw values to the days of the week in our enum like this:

enum DayOfTheWeek: Int {
    case monday = 1
    case tuesday, wednesday, thursday, friday, saturday, sunday
}

Enter fullscreen mode Exit fullscreen mode

As Swift knows monday has the raw value of 1, it will infer the raw values of the following days according to the order in which they’re written. So even though we didn’t specify it, Swift knows that wednesday is 3 and friday is 5.

For other types like String, Swift can infer raw values based on the case name, as shown below:

enum DayOfTheWeek: String {
    case monday,tuesday, wednesday, thursday, friday, saturday, sunday
}

Enter fullscreen mode Exit fullscreen mode

By using String in place of Int , there’s no need to assign a numerical value to even one case, as the associated value will be the name of the case itself. So DayOfTheWeek.monday.rawValue has the String value of monday.

Now, it’s crucial to be aware that no two cases within an enum can have the same raw value as each must be unique. To demonstrate:

enum MyNewEnum: Int {
    case aCase = 1
    case anotherCase = 1
}

Enter fullscreen mode Exit fullscreen mode

Here, both cases within our enum have been assigned the raw value of 1, which would result in a Raw value for enum case is not unique error and wouldn’t even be able to compile our app.

Primary supported types

Raw values can be of integer, floating-point number, string or character types and we’ll now go into more detail:

  • Integer types : Such as Int, Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32, UInt64. For example:
enum Weekday: Int {
    case Sunday = 1
    case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
}

Enter fullscreen mode Exit fullscreen mode
  • Floating-point types : Such as Float, Double. For example:
swiftCopy code
enum Temperature: Double {
    case Celsius = 25.0
    case Fahrenheit = 77.0
    case Kelvin = 298.15
}

Enter fullscreen mode Exit fullscreen mode
  • String type : For example:

enum CompassPoint: String {
    case north, south, east, west
}

Enter fullscreen mode Exit fullscreen mode
  • Character type : For example:

enum ASCIIControlCharacter: Character {
    case tab = "\t"
    case lineFeed = "\n"
    case carriageReturn = "\r"
}

Enter fullscreen mode Exit fullscreen mode

Custom types

While not commonly used, we can also define our own custom types, as long as they adopt any of the following Swift protocols:

  • ExpressibleByIntegerLiteral
  • ExpressibleByFloatLiteral
  • ExpressibleByStringLiteral
  • ExpressibleByUnicodeScalarLiteral

Using a custom type is much more complex than using a primary type, and also requires us to implement RawRepresentable. Below is an example of a custom type implementation:

struct MyNumber: ExpressibleByIntegerLiteral {
    let value: Int

    init(integerLiteral value: Int) {
        self.value = value
    }
}

enum MyNewEnum: RawRepresentable {
    case one
    case two
    case three

    typealias RawValue = MyNumber

    init?(rawValue: MyNumber) {
        switch rawValue.value {
        case 1:
            self = .one
        case 2:
            self = .two
        case 3:
            self = .three
        default:
            return nil
        }
    }

    var rawValue: MyNumber {
        switch self {
        case .one:
            return MyNumber(value: 1)
        case .two:
            return MyNumber(value: 2)
        case .three:
            return MyNumber(value: 3)
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Like we said, custom types aren’t often used but it’s good to know they’re there to help our understanding. Now, let’s take a look at using associated values.

Swift enum associated values

While we use raw values to identify and define the type and value of cases within an enum, we can use Associated Values to attach additional data to enum cases which are dynamic and whose values are not predefined.

Let’s demonstrate with an example:

enum ButtonTapAnalytics {
    case login
    case forgotPassword
    case tableItem1
    case tableItem2
    case tableItem3
}

Enter fullscreen mode Exit fullscreen mode

Here we’ve created an enum called ButtonTapAnalytics with five cases, three of which will be drawn from a table. This would work if the table contents were static, but for dynamic content, where the numerical values cannot be predefined, we’d need to use Associated Values.

Continuing with our example:

enum ButtonTapAnalytics {
    case login
    case forgotPassword
    case tableItem(index: Int)
}

Enter fullscreen mode Exit fullscreen mode

Here we’ve used Associated Values for the tableItem index instead of predefined numerical raw values.

We can now add associated values to cases within our enum, which is fantastic. But what about accessing them? Back to our example:

func log(buttonTapAnalytic: ButtonTapAnalytics) {
    switch buttonTapAnalytic {
    case .login:
        MyAnalyticFramework.log("loginTap")
    case .forgotPassword:
        MyAnalyticFramework.log("forgotPasswordTap")
    case .tableItem(let index):
        MyAnalyticFramework.log("tableItem\(index)Tap")
    }
}

Enter fullscreen mode Exit fullscreen mode

As you can see, we’ve now added code to write the analytics to our framework.

Good job! Let’s see what else we can do with enums.

Enums with raw and associated values

Now, while it’s true that by default we cannot have both raw and associated values in an enum, there are hacks we can use to achieve this if we need to. The trick is to implement RawRepresentable in an enum extension, and then go ahead and define our Raw Values ourselves.

Let’s see how this would look if we applied it to our example enum case:

enum ButtonTapAnalytics {
    case login
    case forgotPassword
    case tableItem(index: Int)
}

extension ButtonTapAnalytics: RawRepresentable {
    public typealias RawValue = String

    //It requires a failable init implementation, which we don't care about
    init?(rawValue: String) {
        return nil
    }

    //Raw Value implementation
    public var rawValue: RawValue {
        switch self {
        case .login:     
            return "loginTap"
        case .forgotPassword:
            return "forgotPasswordTap"
        case .tableItem(let index):
            return "tableItem\\(index)Tap"
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Once we have this, we can rewrite the code to add analytics to our framework, as follows:

func log(buttonTapAnalytic: ButtonTapAnalytics) {
   MyAnalyticFramework.log(buttonTapAnalytic.rawValue)
}

Enter fullscreen mode Exit fullscreen mode

As you can see, our code is now much simpler and much more elegant.

Enum methods

As well as raw and the associated value, we can also define methods in enums. Methods allow us to add functionality directly to an enum, which comes in handy if we need to set up actions related to the enumeration or its cases.

To demonstrate this, let’s go back to an enum case that are days of the week, although this time abbreviated versions, like this:

enum Weekday {
    case sun, mon, tue, wed, thu, fri, sat
}

Enter fullscreen mode Exit fullscreen mode

Now, imagine parts of our app need the index of the day of the week, but other parts need the full weekday String. In this situation we could add one of them as a raw value, while using a method that returns a custom value for the other.

For example, if we had Int as a raw value:

enum Weekday: Int {
    case sun, mon, tue, wed, thu, fri, sat

    func stringRawValue() -> String {
        switch self {
        case .sun:
            return "Sunday"
        case .mon:
            return "Monday"
        case .tue:
            return "Tuesday"
        case .wed:
            return "Wednesday"
        case .thu:
            return "Thursday"
        case .fri:
            return "Friday"
        case .sat:
            return "Saturday"
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Now, if we use Weekday.tue.rawValue we get 2, or we can use Weekday.tue.stringRawValue to get Tuesday.

It’s important to remember her that, since we didn’t define any of the numbers ourselves, Swift infers them starting from 0.

Alternatively, we could have the raw value as a String, like this:

enum Weekday: String {
    case sun = "Sunday"
    case mon = "Monday"
    case tue = "Tuesday"
    case wed = "Wednesday"
    case thu = "Thursday"
    case fri = "Friday"
    case sat = "Saturday"

    func intRawValue() -> Int {
        return [.sun, .mon, .tue, .wed, .thu, .fri, .sat].firstIndex(of: self) ?? 0
    }
}

Enter fullscreen mode Exit fullscreen mode

This way we can use Weekday.tue.rawValue to get Tuesday, or Weekday.tue.intRawValue to get 2.

That about covers the theory behind enums but before we finish, let’s take a look at some examples of enum usage in the real-world.

Real-world examples of enum usage

Finally, let’s look at how enums might be deployed in some real-world situations we regularly encounter while developing apps.

State management

Enums are a great tool for managing app states in Swift and making coding simpler and safer and they are commonly used to set View states, as shown below:

enum MyViewState {
    case loading, loaded, error
}

Enter fullscreen mode Exit fullscreen mode

Now it can be used in the respective View, like this:

switch viewState {
    case .loading:
        return MyLoadingView()
    case .loaded:
        return MyLoadedView()
    case .error:
        return MyErrorView()
}

Enter fullscreen mode Exit fullscreen mode

Error handling

Enums are frequently used to handle errors in Swift and can be used to represent different types of errors. For example, we can have an enum for a network error, which we would set up like this:

enum NetworkError: Error {
    case invalidURL
    case requestFailed
    case invalidResponse
    case invalidData
    case decodingFailed
        case unknownError(code: Int)
}

Enter fullscreen mode Exit fullscreen mode

Here we’ve used NetworkError to represent the most well known errors and a different error for anything unknown.

Let’s say we’re using BugFender on our app, with an enum like this we can simply have a method to send errors to BugFender, as below:

func sendError(error: NetworkError) {
    let title = "Network Error"
    switch error {
    case .unknownError(code: let code):
        Bugfender.sendIssue(title: title, text: "Unknown error with code: \(code)")
    default:
        Bugfender.sendIssue(title: title, text: error.rawValue)
    }
}

Enter fullscreen mode Exit fullscreen mode

API responses

One of the most common use cases for enums is for handling API responses. Building on the NetworkError example above we can create a different enum, like this:

enum APIResult {
    case success(data: Data)
    case error(error: NetworkError)
}

Enter fullscreen mode Exit fullscreen mode

Here our Swift enumeration can be used for any Response from an API which, if successful, could return .success(data) or, in case of an error, could return .failure(.invalidURL).

These are just three of the most common use cases for enums but there are many more possibilities out there.

To sum up

Enums are used to define a group of related values in order to represent a fixed set of possible values, making them a powerful data type in Swift.

  • Enums are created using the enum keyword to which we then add cases to represent values.
  • Enums can have raw values of any type, such as strings, characters, or numeric types. Raw values must be unique within the enum declaration
  • Enums can also have an associated value, allowing additional data to be attached.
  • It’s also possible to have methods associated with enums
  • Real-world use cases of enums include state management, error handling and API responses

We hope you’ve found this guide useful and feel confident to experiment with enums in your own projects.

💖 💪 🙅 🚩
bugfenderapp
Bugfender

Posted on May 27, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related