Enums in Swift: An Overview
Enumerations, or more commonly referred to as “enums”, are a very powerful tool to have in an iOS developer’s tool belt. According to the Swift docs, an enum is “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.”
A great example to get us started is directions on a compass. Let’s take that idea and create an enum:
enum Direction {
case west
case east
case north
case south
}
Alternatively, you can also write the enum like this:
enum Direction {
case west, east, north, south
}
As you can see, the Direction enum models the four possible directions. One of the simplest use cases for enums is to use them in switch/conditional statements and do something based on the case that you pass in like this:
func moveTo(direction: Direction) {
switch direction {
case .west:
print("Move west")
case .east:
print("Move east")
case .south:
print("Move south")
case .north:
print("Move north")
}
}
We can then call this function like so:
moveTo(.west) // => prints "Move west" to the console
One important thing to note is that enums are an “or” type, meaning they can only be one thing at a time. In this case, a direction can be either west, east, south, or north. It cannot be two or more directions at the same time.
Raw Values
We learned that we can use a switch statement to execute functionality specific to a particular case, but we can also assign a default value, or raw value, to each case in the enum and further simplify our code. One thing to keep in mind is that when assigning default values, it is important to assign a type to the enum :
enum Direction: String {
case west = "You traveled west"
case east = "You traveled east"
case south = "You traveled south"
case north = "You traveled north"
}func moveTo(direction: Direction) {
print(direction.rawValue)
}moveTo(Direction.east) // => prints "You traveled east"
But, we are not done! If you do not want to access rawValue, you also have the option to define a function /computed property on the enum.
enum Direction: String {
case west = "You traveled west"
case east = "You traveled east"
case south = "You traveled south"
case north = "You traveled north" var travel: String {
self.rawValue
} func travel() -> String {
self.rawValue
}
}print(Direction.east.travel) // => prints "You traveled east"
print(Direction.west.travel()) // => prints "You traveled west"
Associated Values
Having predefined default values on each case in the enum is great, but what if we wanted to specify the values ourselves? This is where associated values come into play. In order to use associated values with enums, we need to state the type we are passing into for an enum case like so:
enum Workout {
case benchPress(sets: Int)
case treadmill(miles: Int)
case deadlifts(sets: Int)
}func logWorkout(workout: Workout) {
switch workout {
case .benchPress(let sets):
print("You benched \(sets) sets" )
case .treadmill(let miles):
print("You ran \(miles) miles")
case .deadlifts(let sets):
print("You performed \(sets) sets of deadlifts")
}
}let benchPress = Workout.benchPress(sets: 5)
let treadmill = Workout.treadmill(miles: 3)
let deadlifts = Workout.deadlifts(sets: 4)print(logWorkout(workout: benchPress)) // => You benched 5 sets
print(logWorkout(workout: treadmill)) // => You ran 3 miles
print(logWorkout(workout: deadlifts)) // => You performed 4 of deadlifts
Replacing Structs with Enums
A powerful, yet lesser-known way to use enums is to model data. If you’ve ever had a struct that had “or” type of properties on it, that would make a great case for you to use an enum. For example, let’s say we want to model sessions held at WWDC using a struct, Session:
Credit to Azam Sharp for the examples below!
struct Session {
let title: String
let speaker: String
let date: Date
let isKeynote: Bool
let isWorkshop: Bool
let isRecorded: Bool
let isJoinSession: Bool
var jointSpeakers: [String]
}let session = Session(
title: "WWDC 2021 Keynote",
speaker: "Tim Cook",
date: Date(),
isKeynote: true,
isWorkshop: false,
isRecorded: true,
isJoinSession: true,
jointSpeakers: ["Tim Cook", "Phil Schiller"]
)
As you can see, we have quite a few properties that may or may not need to be on every Session object. What if the session is not a keynote or if the session only has one speaker? With this type of structure, we lose all flexibility and are forced to pass in a value for properties that are not applicable to the type of session we’re interested in creating. For instance, if we have a session with one speaker, we’ll need to pass in an empty array for joinSpeakers and will have to explicitly indicate that isJoinSession is false. So how can we solve this problem and make the code more maintainable? With enums of course!
Remember how I mentioned earlier that enums can only be one thing at a time? In this scenario, we can see that we have multiple types of sessions that are possible and therefore an enum fits in perfectly. Let’s take a look at our refactored version with an enum:
enum Session {
case keynote(title: String, speakers: [String], date: Date, isRecorded: Bool)
case workshop(title: String, speaker: String, date: Date, isRecorded: Bool)
case normal(title: String, speaker: String, date: Date)
case joint(title: String, speakers: [String], date: Date, isRecorded: Bool)
}func displaySession(session: Session) {
switch session {
case .keynote(let title, let speakers, let date, let isRecorded):
let speakersStr = speakers.joined(separator: ", ")
print("\(title) - \(speakersStr) - \(date) - \(isRecorded)")
case .workshop(let title, let speaker, let date, let isRecorded):
print("\(title) - \(speaker) - \(date) - \(isRecorded)")
case .normal(let title, let speaker, let date):
print("\(title) - \(speaker) - \(date)")
case .joint(let title, let speakers, let date, let isRecorded):
let speakersStr = speakers.joined(separator: ", ")
print("\(title) - \(speakersStr) - \(date) - \(isRecorded)")
}
}let keynote = Session.keynote(title: "WWDC 2021 Keynote", speakers: ["Tim Cook, Phil Schiller"], date: Date(), isRecorded: true)displaySession(session: keynote) // => "WWDC 2021 Keynote - Tim Cook, Phil Schiller - 2021-10-24 04:29:05 +0000 - true"
As you can see above, we no longer need to include values for properties we no longer need. Using associated values for each session type, we are able to indicate exactly what data each session type needs. Much cleaner and better for maintainability, right?
Polymorphism with Enums
With polymorphism, we are able to maximize our flexibility. Polymorphism, the third pillar of OOP, states that a single function, array, dictionary, object, etc. can work with different types. Let’s take common Swift example. If we have more than one type in an array, we have to mark that array as “[Any]”, meaning the array can contain any type. As you may already know, doing so isn’t very… well, Swifty! At compile time, we do not know what the Any type represents and therefore would need to have a switch statement to check against the type we are interested in working with.
Imagine we have an Animal class, a Cat class that inherits from Animal, a Bird class that inherits from Animal, and a Lion class that inherits from Animal. Now, let’s create an array, “animals”, that contains Animal types like so:
let animals = [Animal]()
If we add a cat object, a lion object, and a bird object into that array, we will be able to because of Polymorphism. We know that all three subclass Animal, therefore the compiler is ok with us adding them into the array. As a result, we can now perform methods and access properties that an animal would normally do, like eat() or sleep()!
Let’s take a look at how we can now use Enums to achieve this same goal!
Assume we are creating an array of photos of type, Photo, and videos of type, Video. Here’s how our enum and that array would look:
let medias: [MediaType] = [
MediaType.photo(Photo()),
MediaType.video(Video())
]enum MediaType {
case photo(Photo)
case video(Video)
}
And if we wanted to work with this array, we could do so like this:
for media in medias {
switch media {
case .photo(let photo):
print(photo.id)
case .video(let video):
print(video.id)
}
}
And that’s it! If you ever encounter a scenario where you need more that one type in an array, consider using enums!
Enums instead of subclassing
This is perhaps one of my favorite use cases for enums. As you may already know, subclassing allows us to architect a hierarchy of information. Like we previously saw, we had a base class, Animal, that has common characteristics/behavior that all animals have. From there, we were able to customize/add new behaviors in Animal’s derivate subclasses.
However, as we start to expand on this and add new subclasses, we will run into a situation where properties that are on the super/base class will no longer apply to the subclass. What do we do in that case? Well, we would be forced to refactor the subclass and move that property/method onto another subclass that is interested in it. As we start to add more such cases, this problem gets exacerbated over time and things start to get out of hand/messy very quickly. Let’s take a look at a quick example.
Imagine we want to create a class hierarchy for modeling workouts, using Workout as the base class. We will initially start with two subclasses, Run and Elliptical.
class Workout {
let id: Int
let beginTime: Date
let endTime: Date
let distance: Int
let caloriesBurned: Int
}class Run: Workout {
let onTreadmill: Bool
let onTrack: Bool
}class Elliptical: Workout {
let maxIncline: Float
let isHIITProgram: Bool
}
So far so good right? Now let’s assume we wanted to shake things up and add a jumping jacks to the mix. Our JumpingJack class would look something like this:
class JumpingJack: Workout {
let sets: Int
let reps: Int
}
We now have our JumpingJack class that subclasses Workout, but we have a slight problem. It doesn’t make sense to include distance or startTime/endTime on the JumpingJack class, but because it’s subclassing Workout, it must hold that property regardless of whether it needs it. Here’s what a potential refactor of the class hierarchy would look like to accommodate our JumpingJack workout:
class Workout {
let id: Int
let caloriesBurned: Int
}class Run: Workout {
let onTreadmill: Bool
let onTrack: Bool
let distance: Int
let beginTime: Date
let endTime: Date
}class Elliptical: Workout {
let maxIncline: Float
let isHIITProgram: Bool
let distance: Int
let beginTime: Date
let endTime: Date
}class JumpingJack: Workout {
let sets: Int
let reps: Int
}
As a result, we now have to repeat certain properties in subclasses because we added a class that effectively acts as an edge case for our Workout superclass. Clearly, this is not an ideal way to go about fixing our problem. Now, let’s take a look at how enums can get us out of this pickle.
enum Workout {
case run(Run)
case elliptical(Elliptical)
case jumpingJack(JumpingJack)
}
Now we can easily pass around workouts in our app:
func displayWorkout(workout: Workout) {
switch workout {
case .run(let run):
print("Your run workout: \(run)")
case .elliptical(let elliptical):
print("Your elliptical workout: \(elliptical)")
case .jumpingJack(let jumpingJack):
print("Your jumping jack workout: \(jumpingJack)")
}
}
By replacing our Workout superclass with an enum, we are able to avoid a hierarchical structure that we would normally have with pure subclassing. We will be able to continue expanding our Workout enum with new workouts and no longer have to worry about refactoring existing ones. In addition, the workouts can be any type such as struct, class, protocol, or another enum!
Conclusion
I hope that you’ve seen the flexibility and robustness advantages that enums bring to the table! You’ve learned about raw values, associated values, and how to replace structs and subclasses with enums. Now, I encourage you to take the next step and apply your newfound knowledge to your next projects!
References:
Swift Docs: https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html
Code with Chris: https://medium.com/p/e5631157972e/edit
Swift in Depth: https://www.amazon.com/Swift-Depth-Tjeerd-t-Veen/dp/1617295183
Azam Sharp’s Swift for Intermediate and Advanced iOS Developers Udemy Course: https://www.udemy.com/course/swift-for-intermediate-and-advanced-ios-developers