Cleaning Up Swift Enumeration Evaluation

Just some quick and simple Swift stuff in this post around writing cleaner code. It’s pretty common to see a pattern like the following in existing code when evaluating enumerations.

enum IceCreamFlavor {
    case chocolate
    case chocolateChip
    case coffee
    case peach
    case strawberry
    case vanilla
}
let currentFlavor: IceCreamFlavor = .chocolate

///...code...

// Long-winded test to determine if flavor is basic, non-premium flavor.
// Let's fix this!!
let isBasicFlavor = currentFlavor == .strawberry || currentFlavor == .chocolate || currentFlavor == .vanilla   
print(isBasicFlavor) //True

While the above code works, it’s also not very legible, is repetitive, and can be hard to maintain as well as error-prone if multiple enum values need to be tested. So how can we make this better?

Using a Function and switch

It’s always important to consider your different options when coding and weigh the pros and cons of each approach. Let’s step through some different ways to make our code better!

One of the simplest scenarios to try would be to use the switch statement to at least make things more readable and reduce repetitive code. For example:

func isBasicFlavor(_ flavor: IceCreamFlavor) -> Bool {
    switch flavor {
    case .strawberry, .chocolate:
        return true
    default:
        return false
    }
}

print(isBasicFlavor(currentFlavor))

That’s a little better and encapsulates the logic; but we might be better served adding this function to the enum itself so that any caller could use it more simply and conveniently.

Using Computed Properties

Creating a computed property called isBasicFlavor on the enum lets us do just that.

enum IceCreamFlavor {
    case chocolate
    case chocolateChip
    case coffee
    case peach
    case strawberry
    case vanilla
    
    var isBasicFlavor: Bool {
        switch self {
        case .strawberry, .chocolate, .vanilla:
            return true
        default:
            return false
        }
    }
}

let isBasicFlavor = currentFlavor.isBasicFlavor
print(isBasicFlavor)

There are still other ways to make this even better and replace the possibly clunky switch statement. One approach would be to use a static array as the computed property and the contains statement to evaluate it from the calling code (and maybe show that list of basic flavors in the calling app as a bonus!).

enum IceCreamFlavor {
    case chocolate
    case chocolateChip
    case coffee
    case peach
    case strawberry
    case vanilla
    
    static var basicFlavors: [IceCreamFlavor] {
        return [.chocolate, .vanilla, .strawberry]
    }    
}

let isBasicFlavor = IceCreamFlavor.basicFlavors.contains(currentFlavor)
print(isBasicFlavor)

Still Another Option Leveraging Extensions

Another method could be to leverage the raw value of the enum to perform some form of comparison with a Swift extension. This can come in handy when you don’t control the source code or values from the extension itself (which might be in someone else’s library). While we currently cannot create a Swift extension for an enum directly, we can create one on a protocol associated with the enum.

In our example below, we have based the enum on the String type which as a raw value type means the enum will now conform to the RawRepresentable protocol. This then allows us to create a simple extension like that essentially does the reverse of the previous example.

enum IceCreamFlavor: String {
    case chocolate = "Chocolate"
    case chocolateChip = "Chocolate Chip"
    case coffee = "Coffee"
    case peach = "Peach"
    case strawberry = "Strawberry"
    case vanilla = "Vanilla"
    
    static var basicFlavors: [IceCreamFlavor] {
        return [.chocolate, .vanilla, .strawberry]
    }
}

extension RawRepresentable where RawValue: Equatable {
    /// Evaluate if input is present in array of values
    /// - Parameter input: Input
    /// - Returns: Boolean true if the input present in array
    func isContained(in input: [RawValue]) -> Bool {
        return input.contains(rawValue)
    }
}

let isBasicFlavor = currentFlavor.isContained(in: IceCreamFlavor.basicFlavors.map({ $0.rawValue }))   
print(isBasicFlavor)

This example was a little convoluted and is, in my opinion, less satisfactory but does present yet another way to skin the proverbial cat or deal with certain cases. Because the extension relies on RawRepresentable, similar functionality would work with many different types of enumerations (or custom objects implementing Equatable and RawRepresentable).

Where To Go Next?

This article will hopefully help you explore some options around Swift enumerations and some of the available tools. I’d also encourage you to explore the Swift language more deeply as well as look at other possibilities for working with enumerations such as the CaseIterable protocol, introduced a couple years ago in Swift 4.2 and Xcode 10. Try listing all of the ice cream flavors above directly from the enum with CaseIterable and allCases for example!

More importantly it’s much more important (and fun!) to realize that coding is an evolution, that there are always different approaches (both better and worse) to solving problems, and that we can always make our code better and learn at the same time! With WWDC 2020 just around the corner, knowing and believing this will put you ahead of the game.

One thought on “Cleaning Up Swift Enumeration Evaluation

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: