Listing all cases in an enum

Safety over simplicity

Swift language was designed with a focus on “safe by default” approach (do not confuse with “safe by design” — Swift does not prevents you from doing dangerous actions but only encourages for good practices). One of such incentive is a great support for sum and product data types, enum and struct, respectively. These types are great to limit number of potential states during compilation time so we don’t have to write any unit tests of cases that should never occur — you definitely prefer to never reach given state, rather than covering it with an obvious unit test.

Problem statement

Despite enums are super-powerful feature in our toolbox, you may find out that there is no simple and safe way to list all cases in an enum (I mean an enum without associated values, of course). I run into a problem of presenting UI picker with all potential states (modelled as an enum) that user could choose from. I needed just an array but Swift does not provide it out-of-the-box and swift-evolution proposal is still waiting for its turn for a review (detailed discussion and its history is available here).

Workarounds

Very often, Swift community suggests two workarounds to solve my problem (inspiration comes from https://theswiftdev.com/2017/01/05/18-swift-gist-generic-allvalues-for-enums/):

  • make an enum representable by Int and increment a counter until you cannot anymore create a case:
  • cast raw memory representation into an enum case

Both of those workarounds are not ideal: first assumes that developer assigns subsequent values (starting from 1 here) for all enums, while second solution may break when Swift changes enum memory representation (we still lack ABI stability in Swift).

In this post I wish to present another technique to list all of enum cases that is safe and independent of Swift implementation details .

TLDR;
To achieve that we can define dedicated RawRepresentable type for your enum and make this type expressible by String or Int literal. Keep in mind that if we try to initialise an enum using init(rawValue:), Swift takes all values of your RawRepresentable (that have corresponding case) one by one and compare it with your rawValue until finding equal value. Therefore, if we try to initialise with some rawValue that does not match any case, we will have a chance to gather all rawValues with corresponding enum inside == operator implementation.


Dedicated RawRepresentable type

Let’s define our goal: find an array with all cases of BestEnum that is represented by aString value:

Keep in mind that enum BestEnum: String is actually just a syntactic sugar for conforming to RawRepresentable protocol with RawValue = String. Let’s see how could we implement it manually:

We all love language features that generate boilerplate for us (other examples include: Codable protocol in Swift 4 and so awaited auto-synthesized Equatableconformance in Swift 5).

Unfortunately, for auto-conforming to RawRepresentable we can only use String or Int rawValues. If you try to use there your own type (let’s say enum BestEnum: TypeToBeAutoRepresentable), Swift compiler complains with:

raw type ‘TypeToBeAutoRepresentable’ is not expressible by any literal

Error message is actually a great hint and reveals that my previous statement that “only String and Int are possible” was invalid — it is enough that your type has to be expressible by a literal.

Fair enough, let’s define our new type that meets this requirement and call itBestEnumRaw. It has no special logic — it just stores String literal as a property value and allows to initialize it with no argument (I will discuss later why do we need dummyinit()):

If we now try to use this struct as a raw representation(enum BestEnum: BestEnumRaw), swift compiler still complains:

‘BestEnum’ declares raw type ‘BestEnumRaw’, but does not conform to RawRepresentable and conformance could not be synthesized

On the other hand, this error message is a bit misleading: our BestEnumRaw actually conforms to RawRepresentable, why does Swift suggest that conformance cannot be synthesized? Solutions is really simple: let’s conform to Equatable protocol and the error goes away:

So far, so good. Our code compiles and definition of our enum is self-explanatory:


Here comes a trick: let’s think what happens if you try to initialise BestEnum with some BestEnumRaw, let’s sayBestEnum(rawValue: BestEnumRaw())? Inference type says that a result of this expression is BestEnum?, so result of this expression should return someBestEnum case or nil, when provided rawValue does not correspond to any case. One may ask: how does Swift check if a given rawValue corresponds to an existing case? It just iterates over all cases and checks if its representable is equal to the value provided in an initialiser. Don’t be scared, it’s really simple. In our case it checks if:

  • BestEnumRaw() == BestEnumRaw(stringLiteral:"my_case") ?
  • BestEnumRaw() == BestEnumRaw(stringLiteral:"other_case") ?
Now it makes sense why does compiler complain when BestEnumRaw does not conform to Equatable protocol — otherwise it wouldn’t be able to compare both RawRepresentables and therefore impossible to create an enum with init(rawValue:).

Of course both comparisons return false so our initialiser fails and returnsnil. Nevertheless, we had a chance to catch all possible BestEnumRaw instance that actually have corresponding enum case. Ha! By having a list of all instance of raw representation, it is so trivial to create a list of all enum cases!

Catching all rawValues

So let’s add static variable all to our enum as a bucket for all rawValues:

and fill it whenever Swift observers comparison of non-trivialBestEnumRaw:

Do you remember that we have a dummy initializer of BestEnumRaw? Thanks to that if rawString == nil we know that it was not initialized from any literal so does not correspond to any enum.

Implementation is ready: we just need to call dummy initializer BestEnum(rawValue: BestEnumRaw()) and BestEnum.all contains String values of all cases 🎉

If you want to achieve an array of actual cases instead of Strings, just map them to a raw object and later flatMap to an enum.

Skip boilerplate code with third-party library

Solution described above is straightforward and you can implement it within couple of minutes for your enum (have a look on gist for a final code). However, if you feel it is cumbersome to implement custom RawRepresentable type for each enum, you can use third-party library EnumList that does it for you. EnumList provides generic struct that you can use as a RawRepresentable. Let me show you how we can define our sample BestEnum for EnumList:

Just use EnumListStringRaw<T> (or EnumListIntRaw<T>) as raw representable, where T defines a bucket struct to keep all rawValues. I suggest nested struct Values into your enum, so that fetching all values expression is self-explanatory: BestEnum.Values.all.

If you need to parse your enum into/from JSON, EnumList works perfectly with Codable protocol.

Summary

This post describes how to solve particular problem to list all enum cases, which Swift does not provide out-of-the box. Even if you hardly ever need to use it, my aim was to encourage you to find some non-obvious workarounds before compromising Swift safety.

If you think that enum with incrementing Int rawValue is easier to implement, I want to warn you that it works nicely only for Int representation and it can be a trap for new developers not aware of continuous rawValues requirement. On the other hand, playing with withUnsafePointer is quite dangerous since compiler will not warn us if something has changed with in-memory representation— in such a case application would crash during runtime or worse.

Think twice if you ever have to compromise —Compilation-time safety first!