Enum usage in Swift: If case, guard case, fallthrough, associated values, and the CaseIteratable protocol. These terms could sound familiar if you’ve worked extensively with Swift enums. 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 from SwiftUI’s standard library:
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 if we used multiple lines. The enum improves readability and benefits us 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 making an enum for my onboarding pages, like this one for RocketSim:
enum OnboardingPage {
case welcome
case selectXcode
case featuresExplanation
}
In this example, we defined the cases underneath each other. However, you could also define the enum as follows:
enum OnboardingPage {
case welcome, selectXcode, featuresExplanation
}
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 selectXcode
case featuresExplanation
}
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 featuresExplanation
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 featuresExplanation
case selectXcode
var title: String {
switch self {
case .welcome:
return "Welcome to RocketSim"
case .featuresExplanation:
return "RocketSim's Xcode Simulator Features"
case .selectXcode:
return "Select Xcode"
}
}
}
We defined a new property title
and used a switch-case statement to specify 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 .selectXcode:
return "Select Xcode"
default:
return rawValue
}
}
In this case, I’ve returned the raw string value as a default title. This is not ideal, as it will return “featuresExplanation” for the featuresExplanation
case. However, you might have scenarios where a case name directly converts well into a readable title.
You’re only required to define the default
case if you’re not yet iterating over all cases. Otherwise, you’ll see a warning mentioning “Default will never be executed”.
Iterating over all enum cases
Enums become especially powerful once you use the CaseIterable
protocol to iterate over all cases. You’ll first need to add the protocol to your enum definition:
enum OnboardingPage: String, CaseIterable {
case welcome
case featuresExplanation
case selectXcode = "Select Xcode"
}
After adding the protocol, you can access a new allCases
static property to access all defined cases:
OnboardingPage.allCases.forEach { onboardingPage in
print(onboardingPage.rawValue)
}
// Prints:
// welcome
// featuresExplanation
// Select Xcode
Swift Enum Count: Getting the total number of cases
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 enum count of 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 = .selectXcode
print(pageOne == pageTwo) // Prints: false
Enum with parameters and conforming to Equatable
However, you can also define enums with associated values:
enum Delay {
case seconds(Int)
case milliseconds(Int)
case microseconds(Int)
case nanoseconds(Int)
}
Swift automatically provides Equatable conformance for enums if their associated values also conform to the Equatable protocol. For standard types, this conformance is included by default. However, for custom types, you may need to implement Equatable conformance manually.
Imagine having an enum with a case that has a custom type UserInfo
as one of its associated values:
struct UserInfo {
let id: Int
let name: String
}
enum AppNotification {
case message(text: String)
case friendRequest(from: UserInfo)
case systemAlert(title: String, description: String)
}
Since the UserInfo
structure doesn’t conform to Equatable, we can’t compare the enum cases at all:
More specifically, the error reads as follows:
Binary operator ‘==’ cannot be applied to two ‘AppNotification’ operands
To resolve this, we make the UserInfo struct conform to Equatable and explicitly declare the enum’s Equatable conformance:
struct UserInfo: Equatable {
let id: Int
let name: String
}
enum AppNotification: Equatable {
case message(text: String)
case friendRequest(from: UserInfo)
case systemAlert(title: String, description: String)
}
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 do not conform to Equatable
. It’s also the case with the 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.
For JPEG and PNG, we will only see a print statement containing “an image” in its tail, while the GIF print statement will also explain that it’s an animatable image.
Using an enum in a ForEach in SwiftUI
A common use case is to use enums as an argument for a ForEach
in SwiftUI. This allows you to show a view for each case. We’ve demonstrated such example in this article already:
ForEach(OnboardingPage.allCases, id: \.self) { onboardingPage in
Text(onboardingPage.title)
.font(.title)
}
In this example, we’re using .self
as an id for the iteration. You can also decide to implement the Identifiable
protocol for the enum:
enum OnboardingPage: String, CaseIterable, Identifiable {
case welcome
case featuresExplanation
case selectXcode
/// Use the rawValue as the identifier for this enum.
var id: String { rawValue }
var title: String {
switch self {
case .welcome:
return "Welcome to RocketSim"
case .featuresExplanation:
return "RocketSim's Xcode Simulator Features"
case .selectXcode:
return "Select Xcode"
}
}
}
After defining the id
property, you no longer have to define the id parameter inside the ForEach
:
ForEach(OnboardingPage.allCases) { onboardingPage in
Text(onboardingPage.title)
.font(.title)
}
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!