Weekly Swift articles, podcasts and tips by John Sundell.

Five powerful, yet lesser-known ways to use Swift enums

Published on 19 Jul 2020
Basics article available: Enums

Swift’s implementation of enums is arguably one of the most interesting aspects of the language as a whole. From defining finite lists of type-safe values, to how associated values can be used to express model variants, and beyond — there’s a wide range of situations in which Swift enums can be used in really powerful ways.

This week, let’s focus on that “and beyond” part, by taking a look at a few somewhat lesser-known ways in which enums can be used to solve various problems in Swift-based apps and libraries.

Namespaces and non-initializable types

A namespace is a programming construct that enables various types and symbols to be grouped together under one name. While Swift doesn’t ship with a dedicated namespace keyword, like some other languages do, we can use enums to achieve a very similar result — in that we can create a hierarchy of nested types to add some additional structure in certain situations.

For example, Apple’s Combine framework uses this technique to group its many Publisher types into one Publishers namespace, which is simply declared as a case-less enum that looks like this:

enum Publishers {}

Then, each Publisher type is added by extending the Publishers “namespace”, for example like this:

extension Publishers {
    struct First<Upstream>: Publisher where Upstream: Publisher {
        ...
    }
}

Using the above kind of namespacing can be a great way to add clear semantics to a group of types without having to manually attach a given prefix or suffix to each type’s name.

So while the above First type could instead have been named FirstPublisher and placed within the global scope, the current implementation makes it publicly available as Publishers.First — which both reads really nicely, and also gives us a hint that First is just one of many publishers available within the Publishers namespace. It also lets us type Publishers. within Xcode to see a list of all available publisher variations as autocomplete suggestions.

What makes enums particularly useful within this context is that they can’t be initialized, which sends another strong signal that the types that we end up using as namespaces aren’t meant to be used on their own.

Along those same lines, that non-initializable characteristic also makes enums a great option when implementing phantom types — which are types that are used as markers, rather than being instantiated to represent values or objects. The same thing goes for types that only contain static APIs, such as the following AppConfig type, which contains various static configuration properties for an app:

enum AppConfig {
    static let apiBaseURL = URL(string: "https://api.swiftbysundell.com")!
    static var enableExperimentalFeatures = false
    ...
}

Iterating over cases

Next, let’s take a look at how enums can be iterated over using the built-in CaseIterable protocol. While we’ve already explored enum iterations before, let’s take a closer look at how we could use those capabilities when designing reusable abstractions and libraries.

For example, the Publish static site generator that powers this website uses a Website protocol to enable each site to freely configure what sections that it contains, and requires the type used to define those sections to conform to CaseIterable:

protocol Website {
    associatedtype SectionID: CaseIterable
    ...
}

The above is a simplified version of the actual Website protocol, which you can find on GitHub here.

That design in turn enables the library to iterate over each section in order to generate its HTML — without requiring users to manually supply any form of array of other collection to iterate over, since using CaseIterable automatically makes an allCases collection available on all conforming types:

extension Website {
    func generate() throws {
        try SectionID.allCases.forEach(generateSection)
    }

    private func generateSection(_ section: SectionID) throws {
        ...
    }
}

Again, the above is a simplification of the actual implementation of Publish, but the core pattern is still the same.

At the call site, a given website can then declare what sections that it contains simply by defining a nested SectionID enum that conforms to CaseIterable, like this:

struct MyPortfolio: Website {
    enum SectionID: CaseIterable {
        case apps
        case blog
        case about
    }
    ...
}

Custom raw types

That an enum can be backed by a built-in raw type, such as String or Int, is definitely a well-known and commonly used feature, but the same thing can’t be said for custom raw types — which can be really useful in certain situations.

For example, let’s say that we’re working on a content management app, and that we enable each entry within our app to be tagged with a series of tags. Since we’d like to add a bit of extra type safety to our code, we don’t store our tags as plain strings, but rather using a Tag type that wraps an underlying String value. To make that type as easy to use as possible, we still enable it to be expressed using a string literal, just like a raw string would, giving us the following implementation:

struct Tag: Equatable {
    var string: String
}

extension Tag: ExpressibleByStringLiteral {
    init(stringLiteral value: String) {
        string = value
    }
}

Now let’s say that we’d like to define a pre-determined set of higher-level categories that are each backed by a given Tag, to enable us to provide a more streamlined filtering UI within our app. If we were to do that using an enum, it might initially seem like we’d need to revert back to using raw strings for the underlying tags that back each category — but that’s actually not the case.

Since our Tag type can be expressed using literals, and since it conforms to Equatable, we can actually declare our Category enum with our own Tag type as its backing raw type:

enum Category: Tag {
    case articles = "article"
    case videos = "video"
    case recommended
    ...
}

Note how we can choose whether to customize the underlying raw value for each case, or just use the ones that the compiler will synthesize for us — just like we can when using raw strings, or any other built-in type. Pretty cool!

Convenience cases

When designing any kind of API, it’s important to try to strike a nice balance between the structure and consistency of the underlying implementation, as well as making the call sites as clear and simple as possible. To that end, let’s say that we’re working on an animation system, and that we’ve defined a RepeatMode enum that enables easy customization of how many times that an animation should repeat before being removed.

Similar to the Tag type from before, we could’ve just used a simple Int to represent that sort of repeat count, but we’ve opted for the following API in order to make it easy to express common repeat modes, such as once and never:

extension Animation {
    enum RepeatMode: Equatable {
        case once
        case times(Int)
        case never
        case forever
    }
}

However, while the above results in a very neat API (by enabling us to use “dot-syntax”, like .times(5) and .forever), our internal handling code ends up becoming somewhat complex — since we’ll need to handle each RepeatMode case separately, even though all of that logic is really similar:

func animationDidFinish(_ animation: Animation) {
    switch animation.repeatMode {
    case .once:
        if animation.playCount == 1 {
            startAnimation(animation)
        }
    case .times(let times):
        if animation.playCount <= times {
            startAnimation(animation)
        }
    case .never:
        break
    case .forever:
        startAnimation(animation)
    }
}

While there are a number of different approaches that we could take in order to solve the above problem (including using a more free-form struct, rather than an enum), let’s keep our enum-based API for now, while also simplifying our internal implementation and handling code.

To do that, let’s reduce our number of actual cases to two — one that lets us specify a numeric repeat count, and one that tells our system to repeat a given animation forever:

extension Animation {
    enum RepeatMode: Equatable {
        case times(Int)
        case forever
    }
}

Then, to keep maintaining the exact same public API as we had before, let’s augment the new version of our enum with two static properties — one for each of the two cases that we just removed:

extension Animation.RepeatMode {
    static var once: Self { .times(1) }
    static var never: Self { .times(0) }
}

A really cool thing about the above change is that it doesn’t actually require us to change our code at all. Our previous switch statement will keep working just as before (thanks to Swift’s advanced pattern matching capabilities), and our public API remains identical.

However, since we now only have two actual cases to deal with, we can heavily simplify our code once we’re ready to do so — for example like this:

func animationDidFinish(_ animation: Animation) {
    switch animation.repeatMode {
    case .times(let times):
        if animation.playCount <= times {
            startAnimation(animation)
        }
    case .forever:
        startAnimation(animation)
    }
}

The reason we still keep a separate case for forever (rather than using .times(.max)) is that we might want to handle that case somewhat differently — for example in order to perform certain optimizations for animations that we know will never be removed, and to accurately represent an infinite repeat count (because Int.max is technically not forever).

Cases as functions

Finally, let’s take a look at how enum cases relate to functions, and how Swift 5.3 brings a new feature that lets us combine protocols with enums in brand new ways.

⚠️ This final part will cover technologies that are currently in beta as part of Xcode 12, so it’s possible that some of the APIs used will change during the beta period.

As an example, let’s say that we’re currently using the following enum to keep track of the loading state of a given Product model, with associated values for any loaded instance, as well for any Error that was encountered:

enum ProductLoadingState {
    case notLoaded
    case loading
    case loaded(Product)
    case failed(Error)
}

A really interesting aspect of enum cases with associated values is that they can actually be used directly as functions. For example, here we’re enabling a ProductViewModel to be initialized with an optional preloaded Product value, which we then map directly into our .loaded case as if that case was a function:

class ProductViewModel: ObservableObject {
    @Published private(set) var state: ProductLoadingState
    ...

    init(product: Product?) {
        state = product.map(ProductLoadingState.loaded) ?? .notLoaded
    }
    
    ...
}

That feature comes particularly in handy when chaining multiple expressions together, for example when using Combine. Here is a more advanced version of the above ProductViewModel example, which uses the Combine-powered URLSession API to load a product over the network, and then maps the result to a ProductLoadingState value, just like above:

class ProductViewModel: ObservableObject {
    @Published private(set) var state = ProductLoadingState.notLoaded

    private let id: Product.ID
    private let urlSession: URLSession
    
    ...

    func load() {
        state = .loading

        urlSession
            .dataTaskPublisher(for: .product(withID: id))
            .map(\.data)
            .decode(type: Product.self, decoder: JSONDecoder())
            .map(ProductLoadingState.loaded)
            .catch({ Just(.failed($0)) })
            .receive(on: DispatchQueue.main)
            .assign(to: &$state)
    }
}

Swift 5.3 introduces yet another capability that further brings functions and enums closer together, by allowing enum cases to fulfill static protocol requirements. Let’s say that we wanted to generalize the concept of preloading across our app — by introducing a generic Preloadable protocol that can be used to create an already loaded instance of a given type, like this:

protocol Preloadable {
    associatedtype Resource
    static func loaded(_ resource: Resource) -> Self
}

What’s really nice is that in order to make our ProductLoadingState enum conform to the above protocol, we just have to declare what type of Resource that it’s loading, and our loaded case will be used to satisfy our protocol’s function requirement:

extension ProductLoadingState: Preloadable {
    typealias Resource = Product
}

Conclusion

Depending on who you ask, enums are either under-utilized, or over-used in Swift. Personally, I think it’s fantastic that Swift’s enums offer such a wide range of features that can be used in so many different situations, but that doesn’t mean that they’re a “silver bullet” that we should always reach for.

It’s important to remember that enums work best when modeling finite lists, and that other language features — including structs and protocols — might be a better fit when we need to build something a bit more flexible or dynamic. But, at the end of the day, the better our knowledge of Swift’s various language features, the better choices we’ll be able to make within each given situation.

Got questions, comments, or feedback? Feel free to email me, or send me a tweet @johnsundell.

Thanks for reading! 🚀