Articles, podcasts and news about Swift development, by John Sundell.

Avoiding problematic cases when using Swift enums

Published on 27 May 2021
Basics article available: Enums

Swift’s implementation of enums is, without a doubt, one of the most beloved and powerful features that the language has to offer. The fact that Swift enums go way beyond simple enumerations of integer-based constants, and support things like associated values and sophisticated pattern matching, makes them a great candidate for solving many different kinds of problems.

However, there are certain kinds of enum cases that can arguably be good to avoid, as they could lead us to some tricky situations, or make our code feel less “idiomatic” that what we intended. Let’s take a look at a few such cases and how they could be refactored using some of Swift’s other language features.

Instabug

Instabug: Whether it’s crashes, slow screen transitions, delayed network calls, or unresponsive UIs — Instabug automatically gives you all of the logs you need to fix bugs and issues, and to ship high-quality apps. Get started now.

Representing the lack of a value

As an example, let’s say that we’re working on a podcast app, and that we’ve implemented the various categories that our app supports using an enum. That enum currently contains cases for each category, as well as two somewhat special cases that’s used for podcasts that don’t have a category at all (none), as well as a category that can be used to reference all categories at once (all):

extension Podcast {
    enum Category: String, Codable {
        case none
case all
        case entertainment
        case technology
        case news
        ...
    }
}

Then, when implementing features such as filtering, we can use the above enum to perform pattern matching against the Category value that the user selected within the UI (which is encapsulated within a Filter model):

extension Podcast {
    func matches(filter: Filter) -> Bool {
        switch filter.category {
        case .all, category:
            return name.contains(filter.string)
        default:
            return false
        }
    }
}

At first glance, the above two pieces of code might look perfectly fine. But, if we think about it, the fact that we’ve currently added a specific none case for representing the lack of a category is arguably a bit strange, given that Swift does have a built-in language feature that’s tailor-made for that purpose — optionals.

So, if we instead were to turn our Podcast model’s category property into an optional, then we’d get support for representing missing categories completely for free — plus we could now leverage all of the features that Swift optionals support (such as if let statements) when dealing with such missing values:

struct Podcast {
    var name: String
    var category: Category?
    ...
}

Something that’s really interesting about the above change is that any exhaustive switch statements that we were previously using on Podcast.Category values will still keep working just as they did before — since it turns out that the Optional type itself is also, in fact, an enum that uses a none case to represent the lack of a value — meaning that code like the following function can remain completely unchanged (apart from modifying its argument to be an optional):

func title(forCategory category: Podcast.Category?) -> String {
    switch category {
    case .none:
    return "Uncategorized"
    case .all:
        return "All"
    case .entertainment:
        return "Entertainment"
    case .technology:
        return "Technology"
    case .news:
        return "News"
    ...
    }
}

The above works thanks to a bit of Swift compiler magic that automatically flattens optionals when they’re used in pattern matching contexts (such as switch statements), which lets us both address cases within the Optional type itself, as well as cases defined within our own Podcast.Category enum, all within the same statement.

If we wanted to, we could’ve also used case nil instead of case .none, since those are functionally identical in the above type of situation.

Domain-specific enums

Next, let’s turn our attention to our Podcast.Category enum’s all case, which is also a bit strange if we think about it. After all, a podcast can’t belong to all categories simultaneously, so that all case really only makes sense within the context of filtering.

So, rather than including that case within our main Category enum, let’s instead create a dedicated type that’s specific to the domain of filtering. That way, we can achieve a quite neat separation of concerns, and since we’re using nested types, we can have our new enum use the same Category name, only this time it’ll be nested within our Filter model — like this:

extension Filter {
    enum Category {
        case any
        case uncategorized
        case specific(Podcast.Category)
    }
}

Worth noting is that we could’ve chosen to use the optional approach here as well, with nil representing either any or uncategorized, but since there are two potential candidates in this case, we’re arguably making our intent much more clear by using dedicated cases here.

What’s really nice about the above approach is that we can now implement our entire filtering logic using Swift’s pattern matching capabilities — by switching on the filtered category and by then using where clauses to attach additional logic to each case:

extension Podcast {
    func matches(filter: Filter) -> Bool {
        switch filter.category {
        case .any where category != nil,
             .uncategorized where category == nil,
             .specific(category):
            return name.contains(filter.string)
        default:
            return false
        }
    }
}

With all of the above changes in place, we can now go ahead and remove the none and all cases from our main Podcast.Category enum — leaving us with a much more straightforward list of each of the categories that our app supports:

extension Podcast {
    enum Category: String, Codable {
        case entertainment
        case technology
        case news
        ...
    }
}

Custom cases and custom types

When it comes to enums like Podcast.Category, it’s incredibly common to (at some point) introduce some kind of custom case that can be used to handle one-off cases, or to provide forward compatibility by gracefully handling cases that might be added server-side in the future.

One way to implement that would be to use a case that has an associated value — in our case a String representing the raw value of a custom category, like this:

extension Podcast {
    enum Category: Codable {
        case all
        case entertainment
        case technology
        case news
        ...
        case custom(String)
    }
}

Unfortunately, while associated values are incredibly useful in other contexts, this is not really one of them. First of all, by adding such a case, our enum can no longer be String-backed, meaning that we’ll now have to write custom encoding and decoding code, as well as logic for converting instances to and from raw strings.

So let’s explore another approach instead, by converting our Category enum into a RawRepresentable struct, which once again lets us leverage Swift’s built-in logic for encoding, decoding, and handling string conversions for such types:

extension Podcast {
    struct Category: RawRepresentable, Codable, Hashable {
        var rawValue: String
    }
}

Since we’re now free to create Category instances from any custom string that we want, we can easily support both custom and future categories without requiring any additional code on our part. However, to make sure that our code remains backward compatible, and to make it easy to refer to any of our built-in, currently known categories — let’s also extend our new type with static APIs that’ll achieve all of those things:

extension Podcast.Category {
    static var entertainment: Self {
        Self(rawValue: "entertainment")
    }

    static var technology: Self {
        Self(rawValue: "technology")
    }

    static var news: Self {
        Self(rawValue: "news")
    }
    
    ...

    static func custom(_ id: String) -> Self {
        Self(rawValue: id)
    }
}

Although the above change did require some amount of extra code to be added, we’ve now ended up with a much more flexible setup that’s almost entirely backward compatible. In fact, the only updates that we need to make are to code that performs exhaustive switches on Category values.

For example, the title function that we took a look at earlier previously switched on such a value to return a title matching a given category. Since we can no longer get an exhaustive list of each Category value at compile-time, we’d now have to use a different approach to compute those titles. In this particular case we could, for example, see this as an excellent opportunity to move those strings to a Localizable.strings file, and then resolve our titles like this:

func title(forCategory category: Podcast.Category?) -> String {
    guard let id = category?.rawValue else {
        return NSLocalizedString("category-uncategorized", comment: "")
    }

    let key = "category-\(id)"
    let string = NSLocalizedString(key, comment: "")

    // Handling unknown cases by returning a capitalized version
    // of their key as a fallback title:
    guard string != key else {
        return key.capitalized
    }

    return string
}

Another option would be to resolve our localized titles within the Category type itself, and to perhaps also add an optional title property which would enable our server to send pre-localized titles for custom categories that our app doesn’t yet natively support.

Auto-named static properties

As a quick bonus tip, one downside of the above struct-based approach is that we now have to manually define the underlying string raw values for each of our static properties, but that’s something that we could solve using Swift’s #function keyword. Since that keyword will be automatically replaced by the name of the function (or, in our case, property) that its encapsulating function is being called from, that’ll give us the same automatic raw value mapping as when using an enum:

extension Podcast.Category {
    static func autoNamed(_ rawValue: StaticString = #function) -> Self {
        Self(rawValue: "\(rawValue)")
    }
}

With the above utility in place, we can now simply call autoNamed() within each of our built-in category APIs, and Swift will automatically fill in those raw values for us:

extension Podcast.Category {
    static var entertainment: Self { autoNamed() }
static var technology: Self { autoNamed() }
static var news: Self { autoNamed() }
    ...

    static func custom(_ id: String) -> Self {
        Self(rawValue: id)
    }
}

Worth noting, though, is that we have to be a bit careful not to rename any of the above static properties when using that #function-based technique, since doing so will also change the underlying raw value for that property’s Category. However, that’s also the case when using enums, and on the flip side, we’re now also preventing typos and other mistakes that can happen when defining each raw string manually.

Support Swift by Sundell by checking out this sponsor:

Instabug

Instabug: Whether it’s crashes, slow screen transitions, delayed network calls, or unresponsive UIs — Instabug automatically gives you all of the logs you need to fix bugs and issues, and to ship high-quality apps. Get started now.

Conclusion

Swift enums are awesome (in fact, I’ve written over 15 articles on that topic alone), but there are certain situations in which another language mechanism might be a better choice for what we’re looking to build, and it’s always possible that we might need to switch between several different mechanisms and approaches as our project grows and evolves.

Hopefully, this article has given you a few ideas on how those kinds of situations and problems could be solved, and if you have any questions, comments, or feedback, then feel free to reach out via either Twitter or email.

Thanks for reading!