New in Swift 4.2, the compiler can generate a collection of an enum’s cases, relieving you from the error-prone task of maintaining such a list yourself. The Swift Evolution proposal that introduced this feature is SE-0194.
The CaseIterable
protocol
For plain enums without associated values, all you have to do is conform your enum to the new CaseIterable
protocol. The compiler will then generate a static allCases
property for you. This is the example from the standard library documentation:
enum CompassDirection: CaseIterable {
case north
case south
case east
case west
}
CompassDirection.allCases // → [north, south, east, west]
CompassDirection.allCases.count // → 4
allCases
is a collection, so it has all the usual properties and capabilities, like count
, map
, and so on:
let caseList = CompassDirection.allCases
.map { "\($0)" }
.joined(separator: ", ")
// → "north, south, east, west"
Example: Table view sections
Let’s look at another example. When working with iOS table views that have multiple sections, it’s a common pattern to introduce an enum to represent the sections:
enum TableSection: Int {
/// The section for the search field
case search = 0
/// Featured content
case featured = 1
/// Regular content cells
case standard = 2
}
Then, in the table view data source and delegate methods, we can switch over the section
parameter and perform the appropriate action for the section in question.
(Side note: I don’t want to turn this article into a debate about the pros and cons of this approach. It definitely has its limits as the table becomes more complex and your switch
statements grow, but it works well enough for a small example. So let’s run with it.)
Now, you’ll find that we need to return the number of cases in your enum as the number of sections in the table view:
func numberOfSections(in tableView: UITableView) -> Int {
return ... // number of cases. How?
}
The traditional solution was to either hardcode the count or to add another case named sectionCount
(or similar) to the end of the enum. This final case’s raw value would then be magically equal to the number of “real” cases. Neither approach is satisfactory.
CaseIterable
makes this much cleaner. Just adopt the protocol, no implementation needed:
extension TableSection: CaseIterable {}
Now we can return TableSection.allCases.count
in the data source method.
Cases appear in declaration order
SE-0194 does not prescribe a specific order of the values in the allCases
collection, but the documentation for CaseIterable
guarantees it:
The synthesized
allCases
collection provides the cases in order of their declaration.
This means we can safely remove the raw-value integer backing from the TableSection
enum and use a case’s position in the allCases
collection for mapping between section indices and enum cases:
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let section = TableSection.allCases[indexPath.section]
switch section {
case .search: ...
case .featured: ...
case .standard: ...
}
}
Synthesis only works in the same file
Like other compiler-synthesized conformances (Equatable
, Hashable
, Encodable
, Decodable
— the list keeps growing)1, the automatic code generation only works when you declare the conformance in the same file where the type is defined (either on the type definition itself or, new in Swift 4.2, in a same-file extension).
The automatic code synthesis is really a feature for owners of a type, not to make retroactive conformances easier.
Retroactive conformance
Of course, you can always declare the conformance retroactively and provide a manual implementation, but that also means you’re responsible for maintaining it if new cases get added.
As a rule of thumb, think twice before adopting CaseIterable
(and other system protocols) for types you don’t own, especially when it comes to types from Apple frameworks. Your code may break in one of two ways in the future:
- when the original owner of the type adds the conformance themselves in a future release,
- or when the original type changes in a way that breaks your conformance, e.g. when a new case gets added to the type. If you’re lucky, the breakage will manifest in a compile-time error on your side, but it’s entirely possible that it will break silently and you’ll have a hard time finding the bug.
Manual conformance
We’ve seen that the automatic synthesis only works for enums without associated values. This makes sense because adding associated values to an enum makes the number of possible values the enum can have potentially infinite.
Ignoring the utility of having access to an infinite sequence of a type’s possible values for a moment, an infinite list is not representable in the confines of CaseIterable
anyway: the protocol requires allCases
to return a Collection
, and collections must be finite (although this could change in the future2).
But as long as the list of all possible values is finite, we can always implement the protocol manually for enums with associated values.
Example
Here’s an example with two enums, where one plain enum is used in the associated values of another:
enum Intensity: CaseIterable {
case light
case medium
case hard
}
enum Workout {
case resting
case running(Intensity)
case cycling(Intensity)
}
(I know “resting” isn’t a real workout type, but bear with me. I wanted an example where not all cases have the same associated value.)
The compiler can synthesize a CaseIterable
conformance for Intensity
but not for Workout
. Let’s write one. We build an array of all possible Workout
values by starting with .resting
and applying each possible associated value to the other two cases:
extension Workout: CaseIterable {
static var allCases: [Workout] {
return [.resting]
+ Intensity.allCases.map(Workout.running)
+ Intensity.allCases.map(Workout.cycling)
}
}
If you find the map
invocations hard to parse, observe that a “naked” enum case without its associated values is equivalent to a constructor function for that particular case. In other words, the type of Workout.running
is (Intensity) -> Workout
, i.e. a function that produces a Workout
value if you give it the desired Intensity
. We pass these constructor functions to map
and get arrays of Workout
back.
This is the result:
Workout.allCases.count // → 7
Workout.allCases
// → [resting, running(light), running(medium), running(hard),
// cycling(light), cycling (medium), cycling(hard)]
Manual maintenance required
The manual implementation is easy enough to write, but it has a major downside compared to compiler-generated code: we now have to remember to keep it up to date. If we later add another workout type to our enum, the compiler won’t alert us that our allCases
implementation is no longer correct. It will just silently return the wrong result.
The only workaround I can think of is a little ugly, but it solves this problem in a really interesting way. Let’s add a local dummy function inside the allCases
body. This function’s only purpose is to switch exhaustively over our enum so that the compiler will complain at this source location when we later add another workout type. We can mark the function as unavailable and give it a parameter of type Never
to document we never intend to call it (since you can’t create a value of type Never
, a function that takes a Never
cannot be called).
extension Workout: CaseIterable {
static var allCases: [Workout] {
/// Dummy function whose only purpose is to produce
/// an error when a new case is added to Workout. Never call!
@available(*, unavailable, message: "Only for exhaustiveness checking, don't call")
func _assertExhaustiveness(of workout: Workout, never: Never) {
switch workout {
case .resting,
.running(.light), .running(.medium), .running(.hard),
.cycling(.light), .cycling(.medium), .cycling(.hard):
break
}
}
return [.resting]
+ Intensity.allCases.map(Workout.running)
+ Intensity.allCases.map(Workout.cycling)
}
}
Observe what happens now when we add another case to the Workout
enum:
It’s like an inline unit test that runs at compile-time! You still have to remember to fix the actual allCases
implementation though, not just the dummy switch
statement.
Beyond enums
The names CaseIterable
and allCases
imply that this feature is specifically intended for enums. However, that doesn’t mean other types can’t adopt the protocol. In fact, this is how the documentation describes CaseIterable
:
A type that provides a collection of all of its values.
This clearly states that any type that has a finite number of values can conform. During the discussion of SE-0194 on Swift Evolution, many people favored names like ValueEnumerable
and allValues
to reflect this broader scope in the name. In the end, the decision was made to derive the name from the primary use case:
The core team felt that these names reflect the primary use case of this protocol, promoting better clarity for most code that iterates over all of the cases.
Making other types CaseIterable
Regardless, let’s conform some non-enum types to CaseIterable
. The simplest example is Bool
3:
extension Bool: CaseIterable {
public static var allCases: [Bool] {
return [false, true]
}
}
Bool.allCases // → [false, true]
Some integer types are also a good match. Note that the return type of allCases
doesn’t have to be an array — it can be any Collection
. It’d be quite wasteful to generate an array of every possible integer when a range is enough:
extension UInt8: CaseIterable {
public static var allCases: ClosedRange<UInt8> {
return .min ... .max
}
}
UInt8.allCases.count // → 256
If your custom type has a finite number of values but the values are expensive to generate, consider returning a lazy collection so as not to perform unnecessary work upfront.
Potential Collection.count
overflow
If we continue to conform the integer types to CaseIterable
, we’ll eventually reach the limits of Swift’s Collection
protocol. The count
of a Collection
is defined to be an Int
, which will overflow as we reach types with more than Int.max
inhabitants (i.e. Int
/UInt
and larger).
You can still create collections that nominally have more elements, so this is fine:
let hugeRange = UInt64.min ... .max
// → ClosedRange<UInt64>
// {lowerBound 0, upperBound 9223372036854775807}
But as soon as you ask this collection for its count
(or more generally, as you compute a distance between two indices that overflows Int.max
), things will not end well.4
Applications
You might ask, when is it useful to have a collection of a type’s inhabitants? We’ve seen a classic use case above in the table view example: the type represents a finite list of options and we want to represent these options in another domain, e.g. as buttons in the UI.
Another application is the need to generate arbitrary values of a certain type. Property-based testing frameworks generate test cases by feeding “random” data into your code and asserting that certain conditions hold. (Often the data isn’t really random but machine-generated following specific rules.)
The SwiftCheck testing framework is based on a protocol named Arbitrary
, which defines an API for types to generate random data. In principle, this is very similar to CaseIterable
, although Arbitrary
has different requirements: it also works for infinitely big types such as strings, and it defines how a value can be shrunk. Once SwiftCheck has found a failing test case, it tries to reduce the value to the simplest form that still fails the test by shrinking it repeatedly.
CaseIterable
and conditional conformance
Many generic types can adopt CaseIterable
by taking advantage of conditional conformance, i.e. the ability to conform a type to a protocol when its generic parameters satify certain requirements (introduced in Swift 4.1).
For example, any Optional
can be CaseIterable
as long as the optional’s generic type is itself CaseIterable
. In code this looks like this (the explicit typealias shouldn’t be necessary, but without it this doesn’t compile in the current Swift 4.2 beta):
extension Optional: CaseIterable where Wrapped: CaseIterable {
public typealias AllCases = [Wrapped?]
public static var allCases: AllCases {
return [nil] + Wrapped.allCases.map { $0 }
}
}
The implementation of allCases
starts with nil
and appends the Wrapped.allCases
collection to it (the mapping step is necessary to convert the elements from Wrapped
to Wrapped?
). I decided to put nil
at the front to follow the CaseIterable
convention of listing values in declaration order — .none
comes before .some
in the standard library source.
Optional
adds one more possible value (namely, .none
or nil
) to the underlying type’s inhabitants. In other words, Optional<Wrapped>.allCases.count
will always be equal to Wrapped.allCases.count + 1
:
CompassDirection.allCases // → [north, south, east, west]
CompassDirection.allCases.count // → 4
CompassDirection?.allCases // → [nil, north, south, east, west]
CompassDirection?.allCases.count // → 5
Notice that although CompassDirection?.allCases
looks like optional chaining syntax, it isn’t! We’re accessing a static member on the type named CompassDirection?
. Alternatively, we could’ve written Optional<CompassDirection>.allCases
.
How useful is it to extend Optional
like this? I’m not sure. It probably doesn’t hurt, but I can’t think of any situation where I’d need this. The Swift core team seems to agree:
The core team discussed this when
CaseIterable
was introduced, and concluded that it wasn’t clearly useful.
-
The compiler also synthesizes
RawRepresentable
conformance in some cases, but that has slightly different semantics (you don’t have to explicitly conform to the protocol). ↩︎ -
Dave Abrahams argues for a remake of the
Sequence
/Collection
hierarchy in the standard library. Among many other things, he proposes to lift the current restriction for collections to be finite.Xiaodi Wu points out that if
CaseIterable.AllValues
were constrained toSequence
,then even types that have an infinite number of possible values could conform to [
CaseIterable
]. Here’s the rub: the definition of a type, or at least one of them, is precisely the set of all possible values of a variable. If unconstrained by finiteness, then all types would meet the semantic requirements of [CaseIterable
]. -
I guess technically,
Never
would be an even simpler example.Void
too, butVoid
is just a typealias for the empty tuple, and tuples can’t be conformed to protocols. ↩︎ -
In Swift 4.1.2 and the current beta of Swift 4.2,
(UInt64.min ... .max).count
actually returns0
, i.e. a wrong result. It really should be a fatal error. I filed a bug, SR-7984. ↩︎