Enum usage in Swift: If case, guard case, fallthrough, and the CaseIteratable protocol. These are all terms which could sound familiar if you’ve worked a lot with enums in Swift. An enumeration defines a common type for a group of related values and enables you to work with those values in a type-safe way within your code.
It’s essential to be aware of enums’ capabilities to improve your code’s readability. You’ll benefit from compile-time safety, allowing fewer bugs to appear. Let’s go over them in more detail.
Defining an enum
Chances are high that you’ve worked with enums before. The text alignment enum is a well-known example:
enum TextAlignment {
case leading
case center
case trailing
}
The above example defines three different cases: leading, center, and trailing. You can use these in SwiftUI by using the multiline text alignment modifier:
VStack {
Text("Hello, world!")
}.multilineTextAlignment(.center)
In this case, we aligned the text to the center in case we’re using multiple lines. The enum improves readability and allows us to benefit from compile-time safety. Enums are great if your method only works with a single case. For example, we can’t align text to the leading and center edges. If you need more options, you can look at OptionSet in Swift explained with code examples.
You can also create your enum, giving it a unique name that does not clash with other types. For example, I prefer creating an enum for my onboarding pages, like this one for RocketSim:
enum OnboardingPage {
case welcome
case grids
case rulers
}
In this example, we defined the cases underneath each other. However, you could also define the enum as follows:
enum OnboardingPage {
case welcome, grids, rulers
}
Defining an enum’s raw value
By default, an enum doesn’t come with a raw value. You can add one by adding an inheritance definition, for example, using a String
type:
enum OnboardingPage: String {
case welcome
case grids
case rulers
}
Doing so allows you to use the case name as a title value:
Text(OnboardingPage.welcome.rawValue) // Shows: "welcome"
As you can see, the case name welcome
converted to a string value “welcome”.
You might want to adjust the title for particular cases in which you can override the raw value as follows:
enum OnboardingPage: String {
case welcome
case grids
case rulers
case selectXcode = "Select Xcode"
}
However, sometimes it might be smarter to define a property using a self-explaining name:
enum OnboardingPage: String, CaseIterable {
case welcome
case grids
case rulers
case selectXcode
var title: String {
switch self {
case .welcome:
return "Welcome to RocketSim"
case .grids:
return "Grids to align"
case .rulers:
return "Rulers for precision"
case .selectXcode:
return "Select Xcode"
}
}
}
We defined a new property title
and used a switch-case statement to define a title for all cases. The compiler will show an error if you forget to handle one of the cases. In case you don’t want to define a specific title for each case, you can choose to use the default statement:
var title: String {
switch self {
case .welcome:
return "Welcome to RocketSim"
case .rulers:
return "Rulers for precision"
case .selectXcode:
return "Select Xcode"
default:
return rawValue
}
}
I’ve returned the raw string value as a default title in this case.
Iterating over all enum cases
Enums become especially powerful once you use the CaseIterable
protocol to iterate over all values. You’ll first need to add the protocol to your enum definition:
enum OnboardingPage: String, CaseIterable {
case welcome
case grids
case rulers
case selectXcode = "Select Xcode"
}
After adding the protocol, you can access a new allCases
property to access all defined cases:
OnboardingPage.allCases.forEach { onboardingPage in
print(onboardingPage.rawValue)
}
// Prints:
// welcome
// grids
// rulers
// Select Xcode
The allCases
property returns a collection type, allowing you to perform actions like forEach
, filter
, and map
. It also allows you to read out the total cases, making it possible to inform your users about the expected number of onboarding pages:
struct OnboardingView: View {
var body: some View {
VStack {
Text("Welcome")
Text("This onboarding contains \(OnboardingPage.allCases.count) pages.")
}
}
}
The best of all: you can iterate over all cases and use a paged tab view to create your dynamic onboarding:
struct OnboardingView: View {
var body: some View {
VStack {
Text("Welcome")
Text("This onboarding contains \(OnboardingPage.allCases.count) pages.")
TabView {
ForEach(OnboardingPage.allCases, id: \.self) { onboardingPage in
Text(onboardingPage.rawValue)
.font(.title)
}
}.tabViewStyle(.page)
}
}
}
Enums and Equatable
Enums are comparable by default, so you don’t have to add the Equatable
protocol conformance:
let pageOne: OnboardingPage = .welcome
let pageTwo: OnboardingPage = .grids
print(pageOne == pageTwo) // Prints: false
However, you can also define enums with associated values:
enum Delay {
case seconds(Int)
case milliseconds(Int)
case microseconds(Int)
case nanoseconds(Int)
}
Swift will support equatable conformance for these enums in the future as well, but at this moment you’ll run into the following error:
More specifically, the error reads as follows:
Binary operator ‘==’ cannot be synthesized for enums with associated values
We can solve this by adding explicit equatable conformance:
enum Delay: Equatable {
case seconds(Int)
case milliseconds(Int)
case microseconds(Int)
case nanoseconds(Int)
}
If case, guard case without Equatable
If your enum contains values of many different types, it might not be easy to inherit from the Equatable
protocol. This is the case when the inner types are not conforming to Equatable
. It’s also the case with the popular Result
enum, which contains generic values.
enum Result<Success, Failure> where Failure: Error {
case success(Success)
case failure(Error)
}
If you’re new to the result enum, I encourage you to read Result in Swift: Getting started with Code Examples.
Both Value
and Error
are generic values and can be of any type. Since we can’t predict the final type of those values, we can also not add equatable conformance.
In these scenarios, you can use the if case
or guard case
statements. For example, you can add an extension to allow easy access to the inner values:
extension Result {
/// The error in case the result was a failure
public var error: Error? {
guard case .failure(let error) = self else { return nil }
return error
}
/// The value in case the result was a success result.
public var value: Success? {
guard case .success(let value) = self else { return nil }
return value
}
}
The extension defines two properties to access the value and error easily. Guard case can be used just like if case
, which makes it possible to read the inner value of the enum case:
/// The value in case the result was a success result.
public var value: Success? {
if case .success(let value) = self {
return value
} else {
return nil
}
}
Using fallthrough in a switch statement
The fallthrough keyword causes program execution to continue from one case in a switch statement to the next case. This can be handy if you’re printing out a type like in the following example:
enum ImageType {
case jpeg
case png
case gif
}
let imageTypeToDescribe = ImageType.gif
var description = "The image type \(imageTypeToDescribe) is"
switch imageTypeToDescribe {
case .gif:
description += " animatable, and also"
fallthrough
default:
description += " an image."
}
print(description) // The image type gif is animatable, and also an image.
Option sets in Swift
Similar to enums in usage, you can also define an OptionSet in Swift. The major difference is that they allow you to pass in multiple cases as options. You can read more about this in my article OptionSet in Swift explained with code examples.
Conclusion
Enums are a powerful tool when writing code in Swift. They improve readability while making you benefit from compile-time safety. You can create dynamic code solutions using the CaseIterable
protocol and custom raw values. Enums are comparable by default unless you’ve added associated values.
If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.
Thanks!