Bugfender
Posted on May 27, 2024
In Swift, an enum (short for enumeration) is a powerful feature that allows us to define a data type with a fixed set of related values so we can work with those values in a type-safe way within our code. In this article we’ll be taking a closer look at Swift enums and their applications in Swift, as well as providing some real-world examples of how we could deploy them in our builds.
We’ll be covering:
- Creating and using Swift enums
- Enums raw values
- Enums associated values
- Enums with raw and associated values
- Swift Enum methods
- Real-world examples of enum usage
We hope by the end of the article you’ll understand enums and how they’re used, and feel confident to give them a try in your own projects. Right, let’s get going…
Creating and using Swift enums
Creating a simple Swift enum is pretty straightforward, we simply use the enum
keyword, for example:
enum DayOfTheWeek {
case monday, tuesday, wednesday, thursday, friday, saturday, sunday
}
In this example:
-
DayOfTheWeek
– is the name of the enum -
monday/tuesday/wednesday/thursday/friday/saturday/sunday
– are the values defined within the enum
It’s worth pointing out here that with enums, ‘values’ are also referred to as ‘cases’, which is why we’ve used the case
keyword when declaring the values inside our enum.
Now we’ve created our enum, we can use .
notation to attribute values to any variable and Swift is smart enough to infer exactly what we’re trying to do:
var aDay: DayOfTheWeek
...
aDay = .thursday
Another advantage is, since they’re constant values, we can easily use a Switch
statement to transfer control.
For example, we could set up a method that prints to the console the day of the week, depending on which DayOfTheWeek
it receives, like this:
func print(day: DayOfTheWeek) {
switch day {
case .monday: print("Monday")
case .tuesday: print("Tuesday")
case .wednesday: print("Wednesday")
case .thursday: print("Thursday")
case .friday: print("Friday")
case .saturday: print("Saturday")
case .sunday: print("Sunday")
}
}
And that’s the basics, good job. Now let’s build on that a little further.
Swift enums raw values
We would add a rawValue
to an enum to associate it with a certain type
, allowing us to use the enum cases as values of that type
. To better understand this, let’s look at our example again:
enum DayOfTheWeek: Int {
case monday = 1
case tuesday = 2
case wednesday = 3
case thursday = 4
case friday = 5
case saturday = 6
case sunday = 7
}
Here we’ve defined our enum as Int
(this is so its cases can only include integer raw values) and assigned numerical values to each of our cases (days of the week) – these values are called raw values.
Now, if we queried DayOfTheWeek.wednesday.rawValue
, we would get 3, as that’s the raw value associated with Wednesday.
Automatic inference
While in our example we assigned each case in our enum with a raw value individually, Swift is intuitive and can infer raw values for all cases if just one is assigned.
For example, we could have assigned numerical raw values to the days of the week in our enum like this:
enum DayOfTheWeek: Int {
case monday = 1
case tuesday, wednesday, thursday, friday, saturday, sunday
}
As Swift knows monday
has the raw value of 1, it will infer the raw values of the following days according to the order in which they’re written. So even though we didn’t specify it, Swift knows that wednesday
is 3 and friday
is 5.
For other types like String
, Swift can infer raw values based on the case name, as shown below:
enum DayOfTheWeek: String {
case monday,tuesday, wednesday, thursday, friday, saturday, sunday
}
By using String
in place of Int
, there’s no need to assign a numerical value to even one case, as the associated value will be the name of the case itself. So DayOfTheWeek.monday.rawValue
has the String
value of monday
.
Now, it’s crucial to be aware that no two cases within an enum can have the same raw value as each must be unique. To demonstrate:
enum MyNewEnum: Int {
case aCase = 1
case anotherCase = 1
}
Here, both cases within our enum have been assigned the raw value of 1, which would result in a Raw value for enum case is not unique
error and wouldn’t even be able to compile our app.
Primary supported types
Raw values can be of integer, floating-point number, string or character types and we’ll now go into more detail:
-
Integer types : Such as
Int, Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32, UInt64
. For example:
enum Weekday: Int {
case Sunday = 1
case Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
}
-
Floating-point types : Such as
Float
,Double
. For example:
swiftCopy code
enum Temperature: Double {
case Celsius = 25.0
case Fahrenheit = 77.0
case Kelvin = 298.15
}
- String type : For example:
enum CompassPoint: String {
case north, south, east, west
}
- Character type : For example:
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
Custom types
While not commonly used, we can also define our own custom types, as long as they adopt any of the following Swift protocols:
ExpressibleByIntegerLiteral
ExpressibleByFloatLiteral
ExpressibleByStringLiteral
ExpressibleByUnicodeScalarLiteral
Using a custom type is much more complex than using a primary type, and also requires us to implement RawRepresentable
. Below is an example of a custom type implementation:
struct MyNumber: ExpressibleByIntegerLiteral {
let value: Int
init(integerLiteral value: Int) {
self.value = value
}
}
enum MyNewEnum: RawRepresentable {
case one
case two
case three
typealias RawValue = MyNumber
init?(rawValue: MyNumber) {
switch rawValue.value {
case 1:
self = .one
case 2:
self = .two
case 3:
self = .three
default:
return nil
}
}
var rawValue: MyNumber {
switch self {
case .one:
return MyNumber(value: 1)
case .two:
return MyNumber(value: 2)
case .three:
return MyNumber(value: 3)
}
}
}
Like we said, custom types aren’t often used but it’s good to know they’re there to help our understanding. Now, let’s take a look at using associated values.
Swift enum associated values
While we use raw values to identify and define the type and value of cases within an enum, we can use Associated Values
to attach additional data to enum cases which are dynamic and whose values are not predefined.
Let’s demonstrate with an example:
enum ButtonTapAnalytics {
case login
case forgotPassword
case tableItem1
case tableItem2
case tableItem3
}
Here we’ve created an enum called ButtonTapAnalytics
with five cases, three of which will be drawn from a table. This would work if the table contents were static, but for dynamic content, where the numerical values cannot be predefined, we’d need to use Associated Values
.
Continuing with our example:
enum ButtonTapAnalytics {
case login
case forgotPassword
case tableItem(index: Int)
}
Here we’ve used Associated Values
for the tableItem
index instead of predefined numerical raw values.
We can now add associated values to cases within our enum, which is fantastic. But what about accessing them? Back to our example:
func log(buttonTapAnalytic: ButtonTapAnalytics) {
switch buttonTapAnalytic {
case .login:
MyAnalyticFramework.log("loginTap")
case .forgotPassword:
MyAnalyticFramework.log("forgotPasswordTap")
case .tableItem(let index):
MyAnalyticFramework.log("tableItem\(index)Tap")
}
}
As you can see, we’ve now added code to write the analytics to our framework.
Good job! Let’s see what else we can do with enums.
Enums with raw and associated values
Now, while it’s true that by default we cannot have both raw and associated values in an enum, there are hacks we can use to achieve this if we need to. The trick is to implement RawRepresentable
in an enum extension, and then go ahead and define our Raw Values
ourselves.
Let’s see how this would look if we applied it to our example enum case:
enum ButtonTapAnalytics {
case login
case forgotPassword
case tableItem(index: Int)
}
extension ButtonTapAnalytics: RawRepresentable {
public typealias RawValue = String
//It requires a failable init implementation, which we don't care about
init?(rawValue: String) {
return nil
}
//Raw Value implementation
public var rawValue: RawValue {
switch self {
case .login:
return "loginTap"
case .forgotPassword:
return "forgotPasswordTap"
case .tableItem(let index):
return "tableItem\\(index)Tap"
}
}
}
Once we have this, we can rewrite the code to add analytics to our framework, as follows:
func log(buttonTapAnalytic: ButtonTapAnalytics) {
MyAnalyticFramework.log(buttonTapAnalytic.rawValue)
}
As you can see, our code is now much simpler and much more elegant.
Enum methods
As well as raw and the associated value, we can also define methods in enums. Methods allow us to add functionality directly to an enum, which comes in handy if we need to set up actions related to the enumeration or its cases.
To demonstrate this, let’s go back to an enum case that are days of the week, although this time abbreviated versions, like this:
enum Weekday {
case sun, mon, tue, wed, thu, fri, sat
}
Now, imagine parts of our app need the index of the day of the week, but other parts need the full weekday String
. In this situation we could add one of them as a raw value, while using a method that returns a custom value for the other.
For example, if we had Int as a raw value:
enum Weekday: Int {
case sun, mon, tue, wed, thu, fri, sat
func stringRawValue() -> String {
switch self {
case .sun:
return "Sunday"
case .mon:
return "Monday"
case .tue:
return "Tuesday"
case .wed:
return "Wednesday"
case .thu:
return "Thursday"
case .fri:
return "Friday"
case .sat:
return "Saturday"
}
}
}
Now, if we use Weekday.tue.rawValue
we get 2, or we can use Weekday.tue.stringRawValue
to get Tuesday
.
It’s important to remember her that, since we didn’t define any of the numbers ourselves, Swift infers them starting from 0
.
Alternatively, we could have the raw value as a String
, like this:
enum Weekday: String {
case sun = "Sunday"
case mon = "Monday"
case tue = "Tuesday"
case wed = "Wednesday"
case thu = "Thursday"
case fri = "Friday"
case sat = "Saturday"
func intRawValue() -> Int {
return [.sun, .mon, .tue, .wed, .thu, .fri, .sat].firstIndex(of: self) ?? 0
}
}
This way we can use Weekday.tue.rawValue
to get Tuesday
, or Weekday.tue.intRawValue
to get 2
.
That about covers the theory behind enums but before we finish, let’s take a look at some examples of enum usage in the real-world.
Real-world examples of enum usage
Finally, let’s look at how enums might be deployed in some real-world situations we regularly encounter while developing apps.
State management
Enums are a great tool for managing app states in Swift and making coding simpler and safer and they are commonly used to set View
states, as shown below:
enum MyViewState {
case loading, loaded, error
}
Now it can be used in the respective View
, like this:
switch viewState {
case .loading:
return MyLoadingView()
case .loaded:
return MyLoadedView()
case .error:
return MyErrorView()
}
Error handling
Enums are frequently used to handle errors in Swift and can be used to represent different types of errors. For example, we can have an enum for a network error, which we would set up like this:
enum NetworkError: Error {
case invalidURL
case requestFailed
case invalidResponse
case invalidData
case decodingFailed
case unknownError(code: Int)
}
Here we’ve used NetworkError
to represent the most well known errors and a different error for anything unknown.
Let’s say we’re using BugFender
on our app, with an enum like this we can simply have a method to send errors to BugFender
, as below:
func sendError(error: NetworkError) {
let title = "Network Error"
switch error {
case .unknownError(code: let code):
Bugfender.sendIssue(title: title, text: "Unknown error with code: \(code)")
default:
Bugfender.sendIssue(title: title, text: error.rawValue)
}
}
API responses
One of the most common use cases for enums is for handling API responses. Building on the NetworkError
example above we can create a different enum, like this:
enum APIResult {
case success(data: Data)
case error(error: NetworkError)
}
Here our Swift enumeration can be used for any Response
from an API
which, if successful, could return .success(data)
or, in case of an error, could return .failure(.invalidURL)
.
These are just three of the most common use cases for enums but there are many more possibilities out there.
To sum up
Enums are used to define a group of related values in order to represent a fixed set of possible values, making them a powerful data type in Swift.
- Enums are created using the
enum
keyword to which we then add cases to represent values. - Enums can have raw values of any type, such as strings, characters, or numeric types. Raw values must be unique within the enum declaration
- Enums can also have an associated value, allowing additional data to be attached.
- It’s also possible to have methods associated with enums
- Real-world use cases of enums include state management, error handling and API responses
We hope you’ve found this guide useful and feel confident to experiment with enums in your own projects.
Posted on May 27, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.