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

Enum iterations in Swift

Published on 26 Aug 2018
Basics article available: Enums

With each new release, Swift keeps getting better and better at creating compiler-generated implementations of common boilerplate. With things like Codable and synthesized conformances to Equatable and Hashable, we can now leverage the compiler to use its knowledge of our types to generate much less error-prone implementations for us.

One of the new features in Swift 4.2 that follows along that same line is the new CaseIterable protocol - that enables us to tell the compiler to automatically synthesize an allCases collection for any RawRepresentable enum. This week, let's take a look at some examples of scenarios in which this new feature can come very much in handy.

Enum dictionaries

In Swift it's very common to use enums to create type-safe keys for dictionaries, for things like configurations and options. Even classes translated from Objective-C have started to adopt this pattern (through some clever translations into enum-like structs), such as NSAttributedString:

let string = NSAttributedString(
    string: "Hello, world!",
    attributes: [
        .foregroundColor: UIColor.red,
        .font: UIFont.systemFont(ofSize: 20)
    ]
)

The major advantage of this pattern is that we no longer have to rely on string constants or integer indexes to pass a dictionary of options to an API - instead we have a clearly defined set of keys that can be type-checked by the compiler.

Let's say that we wanted to use this pattern to define a dictionary of fonts for various text styles that we use in our app - in order to have a single source of truth for all parts of our code that renders text. We might start by defining an enum that represents each type of text that we use, like this:

enum TextType {
    case title
    case subtitle
    case sectionTitle
    case body
    case comment
}

Next, we'll use the above enum to create a dictionary that enables us to look up a UIFont for a given TextType, like this:

let fonts: [TextType : UIFont] = [
    .title : .preferredFont(forTextStyle: .headline),
    .subtitle : .preferredFont(forTextStyle: .subheadline),
    .sectionTitle : .preferredFont(forTextStyle: .title2),
    .comment : .preferredFont(forTextStyle: .footnote)
]

However, we're just getting started, but we've already introduced a bug 😬. Looking closer at the above dictionary, we can see that we've actually missed adding an entry for the .body case, which the compiler can't help us detect.

CaseIterable

Instead of manually defining our font dictionary, like we do above, let's take a look at how Swift 4.2's CaseIterable can help us avoid bugs and make our code more consistent when defining enum-keyed dictionaries.

We'll start by making TextType conform to CaseIterable. Just like with Codable, Equatable and Hashable, we don't need to write any additional code ourselves - instead CaseIterable kind of acts as a marker for the compiler, telling it to synthesize an allCases collection for us:

enum TextType: CaseIterable {
    case title
    case subtitle
    case sectionTitle
    case body
    case comment
}

With the above change, we can now use TextType.allCases to access a collection of all cases in order, which we can then use to build up our fonts dictionary without the risk of missing a case, like this:

var fonts = [TextType : UIFont]()

for type in TextType.allCases {
    switch type {
    case .title:
        fonts[type] = .preferredFont(forTextStyle: .headline)
    case .subtitle:
        fonts[type] = .preferredFont(forTextStyle: .subheadline)
    case .sectionTitle:
        fonts[type] = .preferredFont(forTextStyle: .title2)
    case .body:
        fonts[type] = .preferredFont(forTextStyle: .body)
    case .comment:
        fonts[type] = .preferredFont(forTextStyle: .footnote)
    }
}

Definitely an improvement, since if we now add a new text type without a matching font definition - we'll get a compiler error 👍.

However, we can still improve things further. Not only does the above code look a bit repetitive - since we have to perform the same fonts[type] assignment for all cases - but we also have a bit of a problem at the call site.

Since we're using a dictionary to store our fonts, even though we're now using an exhaustive type for its keys, there's no way for the compiler to guarantee that we'll actually have a UIFont value for each key. So, even though we know that the following will always be non-nil, it'll still be an optional as far as the type system is concerned:

// This will be of type UIFont?, which is not always convenient
let titleFont = fonts[.title]

Let's see if we can solve both of the above two problems at once - using a custom map type.

Enum maps

Our map type will essentially be a thin wrapper around the code that we wrote above, but instead of having to build up a dictionary of values at the call site, we'll simply be able to pass a resolver closure to our map type - which it'll then use to iterate over all cases and build up our collection of values, like this:

struct EnumMap<Enum: CaseIterable & Hashable, Value> {
    private let values: [Enum : Value]

    init(resolver: (Enum) -> Value) {
        var values = [Enum : Value]()

        for key in Enum.allCases {
            values[key] = resolver(key)
        }

        self.values = values
    }

    subscript(key: Enum) -> Value {
        // Here we have to force-unwrap, since there's no way
        // of telling the compiler that a value will always exist
        // for any given key. However, since it's kept private
        // it should be fine - and we can always add tests to
        // make sure things stay safe.
        return values[key]!
    }
}

If we wanted to, we could've also gone the extra mile and made EnumMap conform to Collection, like we did in "Creating custom collections in Swift".

We could've also made EnumMap store its resolver closure and execute it lazily whenever a value is requested, but that would either require it to be a class - because it would need to be mutated - or for us to create a new value on each call to subscript. By making it an immutable struct, lookups will be really fast - even though it does requires a full O(n) iteration when creating it.

With our new EnumMap in place, we can now simply define our fonts collection using a very nice trailing closure-based syntax:

let fonts = EnumMap<TextType, UIFont> { type in
    switch type {
    case .title:
        return .preferredFont(forTextStyle: .headline)
    case .subtitle:
        return .preferredFont(forTextStyle: .subheadline)
    case .sectionTitle:
        return .preferredFont(forTextStyle: .title2)
    case .body:
        return .preferredFont(forTextStyle: .body)
    case .comment:
        return .preferredFont(forTextStyle: .footnote)
    }
}

Even better, all of our font lookups can still use the exact same subscripting syntax, but now result in non-optional UIFont values:

let titleFont = fonts[.title]
let subtitleFont = fonts[.subtitle]

Since we made our EnumMap a generic, it can now also be used in many different places, whenever we want to create a map between an enum and a set of values - without having to deal with any optionals 👍.

Many cases for iterations

The reason CaseIterable was added to Swift 4.2 in the first place, is because iterating over all cases of an enum is such a useful operation in so many situations. Let's take a look at another example, in which we're using an enum to define the various sections of a UITableView-based view controller:

extension ProductListViewController {
    enum Section: String, CaseIterable {
        case featured
        case onSale
        case categories
        case saved
    }
}

One common source of boilerplate when working with a UITableView that has several distinct sections (like this one) is registering cell classes for each section. Just like our first example, when it was easy to cause a bug due to a missing font, it's equally easy to miss registering a cell class for a given section identifier - which (as most of us have experienced) results in a runtime crash.

Let's take a look at how CaseIterable can help us address this problem. Just like how we implemented our EnumMap above, we can define a resolver closure that transforms a given section into a UITableViewCell class, and then use Section.allCases to register the resolved class for each section - like this:

extension ProductListViewController {
    func registerCellClasses() {
        let resolver: (Section) -> UITableViewCell.Type = { section in
            switch section {
            case .featured:
                return FeaturedProductCell.self
            case .onSale:
                return ProductCell.self
            case .categories:
                return CategoryCell.self
            case .saved:
                return BookmarkCell.self
            }
        }

        for section in Section.allCases {
            tableView.register(resolver(section),
                               forCellReuseIdentifier: section.rawValue)
        }
    }
}

That's nice, but we might need to perform similar UITableView class registrations in many different parts of our code base - so we might want to generalize our solution into something that can easily be reused. The good news is that the above code doesn't really need to be aware of the concrete Section type, so we can easily move it to an extension on UITableView itself - like this:

extension UITableView {
    func registerCellClasses<T: CaseIterable & RawRepresentable>(
        for sectionType: T.Type,
        using resolver: (T) -> UITableViewCell.Type
    ) where T.RawValue == String {
        for section in sectionType.allCases {
            register(resolver(section), forCellReuseIdentifier: section.rawValue)
        }
    }
}

We now have a compile-time guarantee that all of our cell classes get registered - and we have one less potential crash to worry about 🎉.

Conclusion

Swift 4.2 is another big step forward for Swift in terms of reducing boilerplate and providing compiler-generated implementations for common tasks that are usually quite error prone when written manually. In general, leveraging the compiler to dynamically generate implementations is a great way for Swift to make up for its current lack of dynamic runtime features - such as a full-featured reflection API.

There are of course many other ways to solve the problems we've taken a look at in this post - with or without enums - but CaseIterable really opens up for some interesting new enum use cases, and combined with exhaustive (default free) switch statements, we can now use enums to add additional compile time safety to many different operations.

What do you think? Are you excited about CaseIterable in Swift 4.2? Have you already started adopting it or is it something you'll try out? Let me know - along with your questions, comments or feedback - on Twitter @johnsundell.

Thanks for reading! 🚀