Steve On Stuff

Exhaustive Collections Of Structs

14 Mar 2019

It’s not uncommon that we need to model a dataset composed of a finite number of discrete data types, where each data type has it’s own unique data model. This is often the case for table view data sources - they’re often comprised of a few different cell types, with each cell type having a different set of properties that it needs to be configured with.

Let’s consider a social networking feed, comprised of text posts, image posts, and article posts. We want only our image posts to have a UIImage associated with them, and only our article posts to have a URL associated with them.

We could model this with an enum:

enum Post {
    case text(String)
    case image(String, UIImage)
    case article(String, URL)
}

The enum feels like a reasonably good fit here because it enforces exhaustiveness. The compiler will force us to consider all of the cases of the enum, and let us know if we missed one.

Associated values also help us tie additional information to each case, and only that case. You have to switch on the enum to extract the associated information, and you only get an image with the image case, and an article with the article case.

We could simply pass that information to a set of table view cells (one for each post type) to create a feed:

func cellForRow(at indexPath: IndexPath) -> UITableViewCell {
    let post = posts[indexPath.row]
    switch post {
        case let .post(text):
            let cell = tableView.dequeueCell(TextCell)
            cell.configure(text)
            return cell
        case let .image(text, image):
            let cell = tableView.dequeueCell(ImageCell)
            cell.configure(text, image)
            return cell
        case let .article(url):
            let cell = tableView.dequeueCell(ArticleCell)
            cell.configure(text, url)
            return cell
    }
}

There’s a few problems with this design though. The first is that is doesn’t scale. You can look forward to this future:

case post(text, url, headline: String, subtitle: String, sharedVia: User?, originalComment: String?, promoted: Bool)

Another issue with relying heavily on enums with associated values is that they’re a pretty unstructured way to organise information. What if you have a view of your timeline that just shows all of the media, without text? You’ll soon end up in underscore hell with this type of thing:

switch Post {
   case .post(_, let image, _, _, _, _):
       //... Show the image cell
}

Just think, every time you added one of those new values you would have to go through your whole codebase, adding yet another underscore to all of your switch statements!

Another serious flaw of the enum strategy is that we can’t represent a single case outside the context of the whole enum type. For instance, if we created an ImagePostComposer, we wouldn’t be able to just return the information for an image post. We’d instead have to return a Post, and then switch on that, leaving us unsure of how to handle the other cases:

let post = imagePostComposer.createPost()

switch post {
    case let .image(text, image):
        // Do something with the post
    default:
        // How should we handle the other cases?
}

Ok, so maybe an enum isn’t the best choice after all. Let’s look at another common way to structure this type of data, using struct`s.

Structs

We can instead model our feed as a collection of structs, like this:

struct TextPost {
    let text: String
}

struct ImagePost {
    let text: String
    let image: UIImage
} 

struct ArticlePost {
    let text: String
    let articleUrl: URL
}

This approach fixes a bunch of the issues that we had with enums.

Creating table view cells looks like this:

func cellForRow(at indexPath: IndexPath) -> UITableViewCell {
    let post = posts[indexPath.row]
    
    if let textPost = post as? TextPost {
        let cell = tableView.dequeueCell(TextCell)
        cell.configure(textPost)
        return cell
    } else if let imagePost = post as? ImagePost {
        let cell = tableView.dequeueCell(ImageCell)
        cell.configure(imagePost)
        return cell
    } else if let articlePost = post as? ArticlePost {
        let cell = tableView.dequeueCell(ArticleCell)
        cell.configure(articlePost)
        return cell
    } else {
        fatalError(Unknown cell type)
    }
}

This approach feels a bit easier to work with than the enum example. We no longer have to unpack each field from the enum case and pass it to the cell separately, instead we can just pass the whole post model.

So is a collection of structs better than an enum then? I’d say so, but whilst this is better in a some ways than the enum, we’ve lost something too. We’re now unable to switch over our post types. We’ve got to add that fatalError in for the default case. We’ve lost exhaustiveness.

This is going to be a problem if we want to add a new post type later on (for instance, a VideoPost). We’re going to be hitting those fatalErrors a lot!

What we want is a way to represent an exhaustive set of structs, and have the compiler warn us when we haven’t considered all cases at compile time, rather than crashing at run time. But we can’t exhaustively switch on a collection of structs, right? Or can we?

Combining the two approaches

enums give us exhaustiveness. structs give us scalability, and the ability to pass around a single case’s data in a way that isn’t tied to other cases.

We can combine these two approaches to get the best of both worlds. We’ll create a Post enum like we did before, and for case we’ll create an associated model struct.

struct TextPostModel {
    text: String
}

struct ImagePostModel {
    text: String
    image: UIImage
}

struct ArticlePostModel {
    text: String
    articleUrl: URL
}

enum Post {
    case text(TextPostModel)
    case image(ImagePostModel)
    case article(ArticlePostModel)
}

Let’s see how that looks when we create our table view cells. Note that we don’t need to extract every field of each case separately this time, we can just pass the model directly to the table view cell:

func cellForRow(at indexPath: IndexPath) -> UITableViewCell {
    let post = posts[indexPath.row]
    switch post {
        case .post(let model):
            let cell = tableView.dequeueCell(TextCell)
            cell.configure(model)
            return cell
        case .image(let model):
            let cell = tableView.dequeueCell(ImageCell)
            cell.configure(model)
            return cell
        case .article(let model):
            let cell = tableView.dequeueCell(ArticleCell)
            cell.configure(model)
            return cell
    }
}

Let’s look at what we’ve achieved:

Leveraging Protocols

Now we have a model for each post, we can also leverage the power of protocols to write more readable code. Remember the example from earlier about creating a feed that only showed images? We can easily build this by conforming some of our models to an ImageProvider protocol:

// Creating a convenience to grab the models will often come in handy
extension Post {
    var model {
        switch self {
            case .text(let model) { return model }
            case .image(let model) { return model }
            case .article(let model) { return model }
        }
    }
}

let images = posts.compactMap { ($0.model as? ImageProvider).image }

The key difference with this setup rather than the ‘enum with associated types’ setup is that we can conform to protocols on the case level. For instance, an ’image’ post, an ’image carousel’ post, and a ‘album’ post could all adopt the ImageProvider protocol, allowing us to easily extract all of the images from all ImageProvider models.

Conclusion

enums provide us with exhaustiveness. Any time we need to model a finite set of types they’re a good fit. Associated values are also great for simple requirements, but they don’t scale well, and having too many leads to code that’s difficult to read.

structs provide a way to model each case individually outside of the context of the other types. This allows us to easily pass around the data associated with a case, and create code that only deal with that single type. Structs also give our data structure (I guess the clue’s in the name!), leading more readable and maintainable code, and the ability to adopt protocols on a case by case basis.

Recently this ‘enum of structs’ pattern has been my go to starting point for modelling this type of data. Most commonly it’s been useful for table view data sources. I’ve found it to be a good pattern for providing type safety, exhastiveness and helping manage complexity. Hopefully it might work for you too!

Next »


« Previous




I'm Steve Barnegren, an iOS developer based in London. If you want to get in touch: