Enums with associated values

Photo by Sharon McCutcheon / Unsplash

Enums, like many other things in Swift, got supercharged. They enable us to do many wonderful things and it would require a much longer article to cover it all. Instead, I will focus on one feature that, in my opinion, really makes a huge difference.

This is regular enum we all know and love:

enum TransportType {
    case bus
    case plane
    case train
    case ship
    case other
}

It's a very convenient way to constrain possible options only to supported values. Consider transport type to be a string instead. The string we need to verify, which can contain anything and where we or the user can make a typo. You can stop crying now. Just remember to thank for enums.

Imagine we make a wallet-like application and we need to display wallet content to the user. To make it simple we allow only credit cards, loyalty cards, tickets, and receipts. These types have data associated with them which we need to read and display:

struct WalletContent {
    let creditCards: [CreditCard]
    let loyaltyCards: [LoyaltyCard]
    let tickets: [Ticket]
    let receipts: [Receipt]
}

struct CreditCard {
    let cardholderName: String
    let number: String
    let expirationDate: Date
    let code: Int
}

struct LoyaltyCard {
    let cardholderName: String
    let storeName: String
    let id: String
}

struct Ticket {
    enum TransportType: String {
        case bus
        case plane
        case train
        case ship
        case other
    }
    
    let transport: TransportType
    let start: String
    let departureDate: Date

    let destination: String
    let arrivalDate: Date
}

struct Receipt {
    let amount: Double
    let name: String
    let date: Date
}

Each structure will be rendered as a separate cell. But we want to also sort the cells and allow users to rearrange them just like in a real wallet. To make it simple we need to store all different content types in an array which we then just display. Types don't match so to be able to do that we need to give up strong typing (at this point Obj-C smiles with satisfaction from the couch) and use Any?:

let anyCells: [Any?] = [CreditCard(cardholderName: "Harry Keogh", number: "1234432112344321", expirationDate: Date(), code: 111),
                        LoyaltyCard(cardholderName: "Harry Keogh", storeName: "Mobius", id: "111111111"),
                        Ticket(transport: .bus, start: "London", departureDate: Date(), destination: "Manchester", arrivalDate: Date()),
                        Receipt(amount: 112, name: "Mobius", date: Date())]

Later when getting concrete cells we need to do typecasting to determine what we pulled out of the can-be-anything bag. It's cumbersome and not very swifty.

What we could do instead is to have one type that can handle all of this unrelated data. This is where enum appears again:

enum WalletContent {
    case creditCard
    case loyaltyCard
    case ticket
    case receipt
}

There is only one problem. Unlike the previous example, this enum sadly doesn't make much sense in this form. Credit card but what number? Loyalty card but where is client id? Ticket for what, from where, when? Clearly, we are missing something. We could display empty cells of the correct types but nothing more.

Fortunately, we can do better. We can add associated values to the cases. Let's redesign our enum a bit:

case creditCard(cardholderName: String, number: String, expirationDate: Date, code: Int)

case loyaltyCard(cardholderName: String, storeName: String, id: String)

case receipt(amount: Double, name: String, date: Date)

The ticket needs TransportType enum to operate. Good thing this is not a problem:

enum TransportType: TransportType {
    case bus
    case plane
    case train
    case ship
    case other
}

case ticket(transport: TransportType, start: String, departureDate: Date, destination: String, arrivalDate: Date)
}

Refactored WalletContent:

enum WalletContent {
    case creditCard(cardholderName: String, number: String, expirationDate: Date, code: Int)
    case loyaltyCard(cardholderName: String, storeName: String, id: String)
    case receipt(amount: Double, name: String, date: Date)
    case ticket(transport: TransportType, start: String, departureDate: Date, destination: String, arrivalDate: Date)
}

Now we can store concrete values in the enum cases so whenever we encounter i.e. .creditCard we will be able to get all the data too.

Now we have all the content under one type therefore we don't have to drop strong typing anymore:

var cells: [WalletContent] = [.creditCard(cardholderName: "Harry Keogh", number: "1234432112344321", expirationDate: Date(), code: 111),
                              .loyaltyCard(cardholderName: "Harry Keogh", storeName: "Mobius", id: "111111111"),
                              .ticket(transport: .bus, start: "London", departureDate: Date(), destination: "Manchester", arrivalDate: Date()),
                              .receipt(amount: 112, name: "Mobius", date: Date())]

Which in turn allows us to switch and do whatever we want to do with a concrete element:

switch cells[1] {
case .creditCard(cardholderName: let cardholderName, number: _, expirationDate: let expirationDate, code: _):
    print("\(cardholderName) you card will expire at \(expirationDate)")
case .loyaltyCard(cardholderName: let cardHolderName, storeName: let storeName, id: let id):
    print("\(cardHolderName) your cliend id for \(storeName) is \(id)")
case .ticket(transport: let transport, start: let start, departureDate: let departureDate, destination: let destination, arrivalDate: _):
    print("Your  travel from \(start) to \(destination) don't miss your \(transport.rawValue) at \(departureDate)")
case .receipt(amount: let amount, name: _, date: let date):
    print("You paid \(amount) at \(date)")
}

The cherry on top is that if at some later point we, or anyone else, add another item i.e. invoice or cash the compiler will complain that we need to handle newly added types too!

Thank you for reading!

P.S. I made the above examples to illustrate my point better. A bit more convenient way of dealing with these types would be:

enum WalletContent {
    case creditCard(data: CreditCard)
    case loyaltyCard(data: LoyaltyCard)
    case receipt(data: Receipt)
    case ticket(data: Ticket)
}

This way you can just reuse models you use for the wallet contents already without any awkward mappings. You could go all the way and instead of model classes, use cell view models since we spoke that we need this to display the content to the user

Kamil Tustanowski

Kamil Tustanowski

I'm an iOS developer dinosaur who remembers times when Objective-C was "the only way", we did memory management by hand and whole iPhones were smaller than screens in current models.
Poland