Weekly Swift articles, podcasts and tips by John Sundell.

Enums

Published on 08 Nov 2019

An enum is a type that enumerates a finite set of values, such as raw values, like strings or integers. They’re really useful when modeling things like options, states, or anything else that can be described using a pre-defined number of values.

Let’s take a look at how enums work in Swift, some of their most prominent features, and examples of situations in which they can come very much in handy.

An enum is defined using the enum keyword, and can contain any number of case declarations that each define one of the enum’s possible states — like how this ContactType enum contains cases for various kinds of contacts:

enum ContactType {
    case friend
    case family
    case coworker
    case businessPartner
}

When using an instance of the above enum, we’ll get a compile-time guarantee that it’ll always be equal to one of the above cases — which is what makes enums such a great match for switch statements (which, by default, have to be exhaustive):

func iconName(forContactType type: ContactType) -> String {
    switch type {
    case .friend:
        return "friend"
    case .family:
        return "family"
    case .coworker:
        return "coworker"
    case .businessPartner:
        return "business_partner"
    }
}

If we add a new case to our enum in the future, then the compiler will give us an error for the above code — which is great, as it forces us to decide how we want to handle the new case.

Our ContactType enum currently doesn’t represent any concrete value, but rather just acts as an abstract representation of a contact type. To change that, we simply have to add a raw type (like String or Int) to our enum declaration — like this:

// By adding ": String" after its name, we've now made our enum
// representable by a raw value — String in this case:
enum ContactType: String {
    case friend
    case family
    case coworker
    // We can also customize what exact raw value that we want a
    // case to be represented by (the default will match the
    // case's name for strings, and its index for integers): 
    case businessPartner = "business_partner"
}

The beauty of the above change is that it enables us to easily convert any ContactType value into a String — meaning that we can now rewrite our iconName function from before to look like this instead:

func iconName(forContactType type: ContactType) -> String {
    return type.rawValue
}

That works both ways, as we can now also easily convert a String value into a ContactType instance as well, given that the string matches the raw value of one of our cases:

// A valid raw value will be matched to its corresponding case,
// while an invalid one will result in 'nil':
let valid = ContactType(rawValue: "coworker") // .coworker
let invalid = ContactType(rawValue: "unknown") // nil

Besides being representable by raw values, enums can also carry associated values — which are values placed within individual enum cases. For example, here we’ve defined a ReadState enum, which is used to describe a user’s progress through a book:

enum ReadState {
    // The user hasn't started reading the book yet:
    case unread
    // The user is currently reading the book at a
    // given page number:
    case inProgress(pageNumber: Int)
    // The user has finished reading the book, and gave it
    // a given rating once done:
    case finished(rating: Rating)
}

Note that an enum cannot have both a raw value type and associated values.

Associated enum values are incredibly flexible, since each case is free to define its very own set of values (like above). That’s particularly useful when modeling state, since rather than having to define all possible state values as optionals, we only have to deal with the values that are relevant for the current state — again something that’s really useful when using a switch statement to handle an enum value:

func restore(fromState state: ReadState) {
    switch state {
    case .unread:
        openBook()
    case .inProgress(let pageNumber):
        openBook()
        turnToPage(number: pageNumber)
    case .finished(let rating):
        displayRating(rating)
    }
}

Finally, let’s take a look at a few different ways to declare an enum value. Another thing that makes enums so nice in Swift is that they support “dot-syntax” — which lets us refer to any enum case simply by prefixing it with a dot, when the enum’s type can be inferred by the compiler.

For example, here’s how we could call the above restore function using dot-syntax:

restore(fromState: .unread)
restore(fromState: .inProgress(pageNumber: 5))

While dot-syntax is arguably just syntactic sugar, it can make our function calls read so much nicer — effectively enabling us to read the above code more or less like normal English sentences.

However, when the underlying type can’t be inferred by the compiler, we do have to declare it — for example when storing an instance of the above ReadState enum as a property:

struct BookSession {
    var book: Book
    var readState: ReadState = .unread
}

We could also have declared the above readState property as ReadState.unread, which would’ve yielded the exact same result.

Swift enums are both incredibly powerful and flexible, and could also be used to simplify our code — by pre-defining the number of states or configurations that a given value can be in at compile-time.

That’s a key part though, because enums are only really useful when the number of states can be specified up-front — so for more free-form values that can only be determined at runtime, other constructs (like structs, protocols, or classes) are most likely going to be more appropriate.

Thanks for reading! 🚀