Enums--or enumerated types--in Swift have been around for a long time, but I never truly explored their potential until I stumbled across an article explaining that "enums can define any hierarchically organized data." So I decided to try using enums, not as I had traditionally used them, but as the very foundation of my data. Today, I invite you to explore the potential of these powerful types with me.
We usually use enums as a way to define a finite set of options in a given context, but in the following scenarios, I'll use them as linked lists (which is easy to do, thanks to indirect cases), and as a simplified sort of function builder (similar to how the new SwiftUI framework functions).
Let's show the power of enums by providing two examples: in the first example, we'll use enums instead of structs and classes to build a fun role-playing game, and in the second example, we'll use enums to create an app for model train enthusiasts.
I love games, especially role-playing games. Nothing is better than entering a world of fantasy and taking your character along a perilous journey, watching them grow, and hopefully, not getting them killed along the way. And if enums can define any hierarchical data, then, we should be able to build game components with them.
For example, in our game, we can have an enum define the possible status conditions that our units can have:
enum Effect {
case poison
case blind
case burn
case bleed
case freeze
case damage(Int)
case heal(Int)
}
Our units can have an array of effects to indicate what happens to them. This enum can also be used for applying effects when an attack happens. Some attacks, such as swords or axes, would only damage units, while other attacks could poison, bleed, or burn our foes.
We can construct enums in such a way that they behave like semantic expressions. By chaining enums together, we can construct easy-to-understand definitions for the components in our game. Let’s say we’d like to create the logic for defining different attacks in our game, such as melee attacks or magic attacks. We can divide the attack into three parts:
- The target
- The effect
- The attack type
So our structure would be something similar to: attack with “type” to “target” for ”effect.“
TARGET
We can start by defining an enum for possible targets:
enum TargetType {
case ally
case enemy
case unit
}
An attack can be against an enemy, an ally (in the case of a heal spell, for example) or any unit. We can also define a given range constraint:
enum Range {
case range(Int)
}
That would limit the range of our attack. We can also define the location of our targets:
enum Direction {
case front
case behind
case around
}
And we can even define a constraint to the number or proximity of targets. For example:
enum Target {
case target(TargetType, Range)
case all(TargetType, Direction, Range)
}
Now, we can construct a very detailed description of the target, say for a melee weapon:
let meleeTarget: Target = .target(.enemy, .inRange(1))
We know that we want to target any enemy that’s within our range. Or if we wanted to perform a massive area-of-effect spell:
let spellTarget: Target = .all(.enemy, .around, .inRange(6))
EFFECT
But what will our attacks do? Well, we can define a trigger, maybe something that happens only if the hit is successful, or if the target dies. Again we can do all of this with enums:
enum TriggerType {
case damage
case hit
case kill
}
The trigger type will tell us when we should trigger the effect of the attack.
enum Trigger {
case end
indirect case after(TriggerType, [Status], Trigger)
}
We can use nested enums to define multiple triggers and construct even more complex attacks. For our melee attack, let’s say that it will deal one point of damage.
let meleeTrigger: Trigger = .after(.hit, [.damage(1)], .end)
Or, we can have it be a poison-coated dagger that can also poison our foes, but only if we dealt damage with it.
let meleeTrigger: Trigger = .after(.hit, [.damage(1)], .after(.damage, [.poison], .end))
With this logic, we can construct any type of effect, and chaining enums lets us mix and match whatever we need.
ATTACKS
Ok, so now that we have our effects and targets, we can define a simple list of attacks:
enum Attack {
case melee(Target, Trigger)
case spell(String, Target, Trigger)
}
Maybe our heal spell is:
let healSpell: Attack = .spell("Heal", .target(.ally, .inRange(4)), .after(.hit, [.heal(1)], .end))
Our fire blast attack is:
let infernoSpell: Attack = .spell("Inferno", .all(.enemy, .around, .inRange(6)), .after(.hit, [.damage(3)], .after(.damage, [.burn], .end)))
We can now build on top of this and define enums for our weapons. Adding associated values can make it so that different weapons of the same type have different damage values (such as a wooden spear versus an iron spear):
enum Weapon {
case spear(dmg: Int)
case poisonDagger
var attack: Attack {
switch self {
case let .spear(dmg):
let meleeTarget: Target = .target(.enemy, .inRange(2))
return .melee(meleeTarget, .after(.hit, [.damage(dmg)], .end))
case .poisonDagger:
let meleeTarget: Target = .target(.enemy, .inRange(1))
return .melee(meleeTarget, .after(.hit, [.damage(1)], .after(.damage, [.poison], .end)))
}
}
}
We can do the same for spells. A magical tome can have a group of spells associated with it. Or we can define a new enum for different enemies in our game. Each enemy can perform different attack actions:
enum Enemy {
case goblin
case dragon
var actions: [Attack] {
switch self {
case .goblin:
return [Weapon.poisonDagger.attack]
case .dragon:
let bite: Attack = .melee(.target(.enemy, .inRange(1)), .after(.hit, [.damage(3)], .after(.kill, [.heal(1)], .end)))
let dracarys: Attack = .spell("Inferno", .all(.enemy, .around, .inRange(6)), .after(.hit, [.damage(3)], .after(.damage, [.burn], .end)))
return [bite, dracarys]
}
}
}
Let’s now look into a different, more practical approach for using enums in apps. Say you’re a train enthusiast, and as such, you're creating an app to manage your train emporium. Again, we can use our trusty enums to create our models. Let’s start by creating an enum for the cargo of our trains. They can either hold some sort of material or be empty:
enum Cargo {
case chemical(String, Int)
case coal(Int)
case ore(Int)
case lumber(Int)
case none
}
Then, to create our train model, we can do something like this:
enum Train {
case tail(id: String, cargo: Cargo)
indirect case car(id: String, cargo: Cargo, next: Train)
}
In this case, we can have an ID for each car, the cargo it hauls, and possibly the next car attached. This is simple and elegant. This also means that we can attach one train to another.
So we can start creating our trains easily:
let train: Train = .car(id: "WX900", cargo: .lumber(20), next:
.car(id: "WX120", cargo: .ore(40), next:
.car(id: "TA763", cargo: .chemical("Radioactive Waste", 20), next:
.tail(id: "T800", cargo: .coal(100)))))
Now, if we want to know the total weight of the cargo, we can add a computed property to our enums:
enum Cargo {
case chemical(String, Int)
case coal(Int)
case ore(Int)
case lumber(Int)
case none
var tons: Int {
switch self {
case .chemical(_, let tons):
return tons
case .coal(let tons), .ore(let tons), .lumber(let tons):
return tons
case .none:
return 0
}
}
}
And a couple more for our train model:
enum Train {
case tail(id: String, cargo: Cargo)
indirect case car(id: String, cargo: Cargo, next: Train)
var numberOfCars: Int {
switch self {
case .car(id: _, cargo: _, next: let next):
return 1 + next.numberOfCars
default:
return 1
}
}
var totalTons: Int {
switch self {
case .car(id: _, cargo: let cargo, next: let next):
return cargo.tons + next.totalTons
case .tail(id: _, cargo: let cargo):
return cargo.tons
}
}
}
So getting those properties becomes something as simple as:
train.numberOfCars // 4
train.totalTons // 180
As you can see, enums are a very powerful type in Swift. They are flexible enough to be used as building blocks for data and simple enough to understand and use. I had never thought of using enums instead of structs and classes, but now I imagine that we might (one day) end up doing enum-oriented programming.