Advanced and Practical Enum usage in Swift
When and how to use enums in Swift? This is a detailed practical overview of all the possibilities enums can offer you.
Introduction
When and how to use enums in Swift? This is a detailed practical overview of all the possibilities enums can offer you.
Similarly to the switch
statement,
enum
's in Swift may at first glance look like a slightly improved
variant of the well known C enum
statement. I.e. a type that
allows you to define that something is "one of something more
general". However, the particular design
decisions behind Swift's enums allow it to be used in a much wider
range of practical scenarios than plain C enums. In particular,
they're great tools to clearly manifest the intentions of your code.
In this post, we'll first look at the syntax and possibilities of using
enum
, and will then use them in a variety of (hopefully) practical,
real world scenarios to give a better idea of how and when to use them.
We'll also look a bit at how enums are being used in the Swift Standard
library.
Before we dive in, here's a definition of what enums
can be. We'll
revisit this definition later on:
"Enums declare types with finite sets of possible states and accompanying values. With nesting, methods, associated values, and pattern matching, however, enums can define any hierarchically organized data."
First, though, what are enums
?
Diving In
A short overview of how to define and use enums.
We're working on a game, and the player can move in four directions. So our player movement is restricted. He can only go right or left. You could model that in the following manner:
if movement == \"left\" { ... }
else if movement == \"right\" { ...}
However this is dangerous, what if we have a typo in our code and
movement
is neither left
nor right
but leeft
?. Wouldn't it be cool
if the compiler would point out if we have a typo like that? You could just write:
let moveLeft = 0
let moveRight = 1
if movement == moveLeft { ... }
else if movement == moveRight { ... }
This would solve our problem of typos in the code, but if we had more movements, it would be an easy bug to forget to handle all the movements. Imagine somebody extends the code in a couple of months and adds two new movements:
let moveUp = 0
let moveDown = 1
This developer forgets to update the if
logic, though, so now we have more
movements but we forgot to handle them. Wouldn't it be great if the compiler would
tell us if we forgot to handle all the cases of our movements? That's what the enum
type is for:
Defining Basic Enums
Enum
s tell Swift that a particular set of cases
belong together. Our movement
enum
could look like this:
enum Movement {
case left
case right
}
It is considered proper style in Swift to always use lowercase for the enum case
name
Swift's switch allows you to handle all the states of an enum
:
let myMovement = Movement.left
switch myMovement {
case Movement.left: player.goLeft()
case Movement.right: player.goRight()
}
If we would add another case
(such as up
), then the compiler would complain.
There's also a really nice shortcut in Swift. Since the compiler knows that myMovement
is
of type Movement
you don't have to write that out explicitly. This also works:
switch myMovement {
case .left: player.goLeft()
case .right: player.goRight()
}
It is considered good style to not write out the enum
name. Theyre may be situations where you have to do it in order to please the Compiler though.
Handling Enums
Besides the switch
statement above, Swift also offers many more ways of handling enum
types. Many of which can be found in our Pattern Matching Guide, some of the will also be handled in this guide on enum
.
Enum values
Sometimes may want to have a value assigned to each enum
case.
This is useful if the enum
itself indeed relates to something which
can be expressed in a different type. C allows you to assign numbers
to enum cases
. Swift gives you much more flexibility here:
// A pretty useless enum
enum Binary {
case zero = 0
case one = 1
}
// You can also map to strings
enum House: String {
case baratheon = \"Ours is the Fury\"
case greyjoy = \"We Do Not Sow\"
case martell = \"Unbowed, Unbent, Unbroken\"
case stark = \"Winter is Coming\"
case tully = \"Family, Duty, Honor\"
case tyrell = \"Growing Strong\"
}
// Or to floating point (also note the fancy unicode in enum cases)
enum Constants: Double {
case π = 3.14159
case e = 2.71828
case φ = 1.61803398874
case λ = 1.30357
}
For String
and Int
types, you can even omit the values and the Swift
compiler will do the right thing:
// mercury = 1, venus = 2, ... neptune = 8
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}
// north = \"north\", ... west = \"west\"
enum CompassPoint: String {
case north, south, east, west
}
Swift supports the following types for the value of an enum:
- Integer
- Floating Point
- String
- Boolean
You can support more types by implementing a specific protocol.
If you want to access the values, you can do so with the rawValue
property:
let bestHouse = House.stark
print(bestHouse.rawValue)
// prints \"Winter is coming\"
However, there may also be a situation where you want to construct an
enum case
from an existing raw value. In that case, there's a special
initializer for enums:
enum Movement: Int {
case left = 0
case right = 1
case top = 2
case bottom = 3
}
// creates a movement.Right case, as the raw value for that is 1
let rightMovement = Movement(rawValue: 1)
If you use the rawValue
initializer, keep in mind that it is a
failable
initializer,
i.e. you get back an
Optional,
as the value you're using may not map to any case at all, say if you
were to write Movement(rawValue: 42)
.
Nesting Enums
If you have specific sub type requirements, you can also logically nest enums in an enum. This allows you to contain specific information on your enum case within the actual enum. Imagine a character in an RPG. Each character can have a weapon, all characters have access to the same set of weapons. All other instances in the game do not have access to those weapons (they're trolls, they just have clubs).
enum Character {
enum Weapon {
case bow
case sword
case lance
case dagger
}
enum Helmet {
case wooden
case iron
case diamond
}
case thief
case warrior
case knight
}
Now you have a hierachical system to describe the various items that your character has access to.
let character = Character.thief
let weapon = Character.Weapon.bow
let helmet = Character.Helmet.iron
If you add initializers for the nested enum
types, you can still benefit from not having
to type out the the long description. Imagine a function that calculates how strong a
character is, based on the character, the weapon, and the helmet:
func strength(of character: Character,
with weapon: Character.weapon,
and armor: Character.Helmet) {
return 0
}
// You can still call it like this:
strength(of: .thief, with: .bow, and: .wooden)
This is still really clear and easy to understand.
Containing Enums
In a similar vein, you can also embed enums in structs
or classes
.
Continuing with our previous example:
struct Character {
enum CharacterType {
case thief
case warrior
case knight
}
enum Weapon {
case bow
case sword
case lance
case dagger
}
let type: CharacterType
let weapon: Weapon
}
let warrior = Character(type: .warrior, weapon: .sword)
This really helps in keeping related information together.
Associated Values
Associated values are a fantastic way of attaching additional
information to an enum case
. Say you're writing a trading engine, and
there're two different possible trade types. buy
and sell
. Each of
them would be for a specific stock and amount:
Simple Example
enum Trade {
case buy
case sell
}
func trade(tradeType: Trade, stock: String, amount: Int) {}
However, the stock and amount clearly belong to the trade in question,
having them as separate parameters feels unclean. You could embed it
into a struct
, but associated values allow for a much cleaner
solution:
enum Trade {
case buy(stock: String, amount: Int)
case sell(stock: String, amount: Int)
}
func trade(type: Trade) {}
This defines two cases, buy
and sell
. Each of these cases has additional
values attached to it: The stock
and amount to buy / sell. These cases cannot exist
without these additional values. You can't do this:
let trade = Trade.buy
You always have to initialize these cases with the associated values:
let trade = Trade.buy(\"APPL\", 500)
Pattern Matching
If you want to access this information, again, pattern matching comes to the rescue:
let trade = Trade.buy(stock: \"AAPL\", amount: 500)
if case let Trade.buy(stock, amount) = trade {
print(\"buy \(amount) of \(stock)\")
}
Here, you're telling the Swift compiler the following:
"If the trade
is of type Trade.buy
with the two values stock
and amount
, then let
those
two variables exist with the values". You kinda have to read this if
line from right to left.
There's an alternative way of writing this with two let
statements:
if case Trade.buy(let stock, let amount) = trade {
...
}
Labels
Associated values do not require labels. You can just denote the types you'd like to see in your enum case
.
enum Trade {
case buy(String, Int)
case sell(String, Int)
}
// Initialize without labels
let trade = Trade.sell(\"APPL\", 500)
If you don't add labels, you also don't write them out when creating a case. If you add them, though, you'll have to always type them out when creating your enum cases.
Use Case Examples
Associated Values can be used in a variety of ways. What follows is a list of short examples in no particular order.
// Cases can have different values
enum UserAction {
case openURL(url: NSURL)
case switchProcess(processId: UInt32)
case restart(time: NSDate?, intoCommandLine: Bool)
}
// Or imagine you're implementing a powerful text editor that allows you to have
// multiple selections, like Sublime Text here:
// https://www.youtube.com/watch?v=i2SVJa2EGIw
enum Selection {
case none
case single(Range<Int>)
case multiple([Range<Int>])
}
// Or mapping different types of identifier codes
enum Barcode {
case UPCA(numberSystem: Int, manufacturer: Int, product: Int, check: Int)
case QRCode(productCode: String)
}
// Or, imagine you're wrapping a C library, like the Kqeue BSD/Darwin notification
// system: https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2
enum KqueueEvent {
case userEvent(identifier: UInt, fflags: [UInt32], data: Int)
case readFD(fd: UInt, data: Int)
case writeFD(fd: UInt, data: Int)
case vnodeFD(fd: UInt, fflags: [UInt32], data: Int)
case errorEvent(code: UInt, message: String)
}
// Finally, all user-wearable items in an RPG could be mapped with one
// enum, that encodes for each item the additional armor and weight
// Now, adding a new material like 'Diamond' is just one line of code and we'll have the option to add several new Diamond-Crafted wearables.
enum Wearable {
enum Weight: Int {
case light = 1
case mid = 4
case heavy = 10
}
enum Armor: Int {
case light = 2
case strong = 8
case heavy = 20
}
case helmet(weight: Weight, armor: Armor)
case breastplate(weight: Weight, armor: Armor)
case shield(weight: Weight, armor: Armor)
}
let woodenHelmet = Wearable.helmet(weight: .light, armor: .light)
Methods and Properties
Swift enum
types can have methods and properties attached to them. This works exactly like you'd do it for
class
or struct
types. Here is a very simple example:
enum Transportation {
case car(Int)
case train(Int)
func distance() -> String {
switch self {
case .car(let miles): return \"\(miles) miles by car\"
case .train(let miles): return \"\(miles) miles by train\"
}
}
}
The main difference to struct
or class
types is that you can switch
on self
within the method
in order to calculate the output.
Here is another, more involved, example where we use the enum values
to determine the numerical attributes of a character in a method.
enum Wearable {
enum Weight: Int {
case light = 1
}
enum Armor: Int {
case light = 2
}
case helmet(weight: Weight, armor: Armor)
func attributes() -> (weight: Int, armor: Int) {
switch self {
case .helmet(let w, let a):
return (weight: w.rawValue * 2, armor: a.rawValue * 4)
}
}
}
let woodenHelmetProps = Wearable.helmet(weight: .light, armor: .light)
.attributes()
Properties
Enums don't allow for adding stored properties. This means the following does not work:
enum Device {
case iPad
case iPhone
let introduced: Int
}
Here, we'd like to store an Apple device together with the year when it was introduced. However, this does not compile.
Even though you can't add actual stored properties to an enum
, you
can still create computed properties. Their contents, of course, can be
based on the enum value or enum associated value. They're read-only though.
enum Device {
case iPad,
case iPhone
var introduced: Int {
switch self {
case .iPhone: return 2007
case .iPad: return 2010
}
}
}
This works great as the year of the introduction of an Apple device never changes.
You couldn't use this if you'd like to store mutable / changing information. In those cases
you'd always use associated values
:
enum Character {
case wizard(name: String, level: Int)
case warior(name: String, level: Int)
}
Also, you can always still add properties for easy retrieval of the associated value
:
extension Character {
var level: Int {
switch self {
case .wizard(_, let level): return level
case .warior(_, let level): return level
}
}
}
Static Methods
You can also have static methods on enums
, i.e. in order to create an
enum
from a non-value type.
Static methods are methods you can call on the name of the type instead of
a specific instance of the type. In this example we add a static method
to our enum Device
which returns the most recently released device:
enum Device {
static var newestDevice: Device {
return .appleWatch
}
case iPad,
case iPhone
case appleWatch
}
Mutating Methods
Methods can be declared mutating
. They're then allowed to change the
case
of the underlying self
parameter. Imagine a lamp that has three states:
off
, low
, bright
where low
is low light and bright
a very strong light.
We want a function called next
that switches to the next state:
enum TriStateSwitch {
case off, low, bright
mutating func next() {
switch self {
case .off:
self = low
case .low:
self = .bright
case high:
self = off
}
}
}
var ovenLight = TriStateSwitch.low
ovenLight.next()
// ovenLight is now equal to .bright
ovenLight.next()
// ovenLight is now equal to .off
Before we look at advanced enum
usage, we'll do a brief recap of what we've
learned in this section so far.
Recap
Lets have another look at the explanation we gave at the beginning and see if it became clearer now.
Enums declare types with finite sets of possible states and accompanying values. With nesting, methods, associated values, and pattern matching, however, enums can define any hierarchically organized data.
The definition is a lot clearer now. Indeed, if we add associated values
and nesting, an enum case
is like a closed, simplified struct
. The
advantage over structs being the ability to encode categorization and
hierachy:
// Struct Example
struct Point { let x: Int, y: Int }
struct Rect { let x: Int, y: Int, width: Int, height: Int }
// Enum Example
enum GeometricEntity {
case point(x: Int, y: Int)
case rect(x: Int, y: Int, width: Int, height: Int)
}
The addition of methods and static methods allow us to attach
functionality to an enum
without having to resort to free functions
// C-Like example
enum Trade {
case buy
case sell
}
func order(trade: Trade)
// Swift Enum example
enum Trade {
case buy
case sell
func order() {}
}
Advanced Enum Usage
The enum
type is one of Swift's most distinctive features. We already saw a lot of different use cases. However, There's much more that enums can do.
They can be used with protocols, just like other Swift types, they can have extensions, they can be generic, and much more. This chapter will introduce these interesting enum
features.
We will start by having a look at conforming enums to protocols.
Protocols
We already mentioned the similarity between the struct
and enum
types. In
addition to the ability to add methods, Swift also allows you to use
Protocols and Protocol Extensions with enums.
Swift protocols define an interface that types can
conform to. In this case our enum
can conform to it. For a start,
let's take a protocol from the Swift standard library.
CustomStringConvertible
is a type with a customized textual
representation suitable for printing purposes:
protocol CustomStringConvertible {
var description: String { get }
}
It has only one requirement, namely a getter for a string. We can implement this on an enum quite easily:
enum Trade: CustomStringConvertible {
case buy, sell
var description: String {
switch self {
case .buy: return \"We're buying something\"
case .sell: return \"We're selling something\"
}
}
}
Some protocol implementations may need internal state handling to cope with the requirements. Imagine a protocol that manages a bank account:
protocol AccountCompatible {
var remainingFunds: Int { get }
mutating func addFunds(amount: Int) throws
mutating func removeFunds(amount: Int) throws
}
You could easily fulfill this protocol with a struct
, but in the
context of your application, an enum
is the more sensible approach.
However, you can't add properties like var remainingFunds: Int
to an
enum
, so how would you model that? The answer is actually easy, you
can use associated values for this:
enum Account {
case empty
case funds(remaining: Int)
case credit(amount: Int)
var remainingFunds: Int {
switch self {
case .empty: return 0
case .funds(let remaining): return remaining
case .credit(let amount): return amount
}
}
}
To keep things clean, we can then define the required protocol functions
in a protocol extension on the enum
:
extension Account: AccountCompatible {
mutating func addFunds(amount: Int) {
var newAmount = amount
if case let .funds(remaining) = self {
newAmount += remaining
}
if newAmount < 0 {
self = .credit(newAmount)
} else if newAmount == 0 {
self = .empty
} else {
self = .funds(remaining: newAmount)
}
}
mutating func removeFunds(amount: Int) throws {
try self.addFunds(amount * -1)
}
}
var account = Account.funds(remaining: 20)
try? account.addFunds(amount:10)
try? account.removeFunds(amount:15)
As you can see, we implemented all the protocol requirements by storing
our values within our enum cases
. A very nifty side effect of this is,
that now you can test for an empty account with a simple pattern match
all over your code base. You don't have to see whether the
remainingFunds
are zero.
We're also implementing the protocol in an extension. We'll learn more about
extensions on enum
types in the next chapter.
Extensions
Take the following enum
:
enum Entity {
case soldier(x: Int, y: Int)
case tank(x: Int, y: Int)
case player(x: Int, y: Int)
}
As we just saw, enums can also be extended. There're two use cases for this.
You've already seen the first one: Conforming to a protocol
.
extension Entity: CustomStringConvertible {
var description: String {
switch self {
case let .soldier(x, y): return \"\(x), \(y)\"
case let .tank(x, y): return \"\(x), \(y)\"
case let .player(x, y): return \"\(x), \(y)\"
}
}
}
The other use case
is keeping enum cases
and methods
separate, so that a
reader of your code can easily digest the enum
and afterwards
read the methods.
extension Entity {
mutating func move(dist: CGVector) {}
mutating func attack() {}
}
Extending
Extensions also allow you to add useful code to existing enum
types. Either from the Swift standard library, or from third party frameworks, or from yourself if you happen to have a big codebase.
For example, we can extend the standard library Optional
type in order to add useful extensions. If you'd like to learn more about this, we have an article that explains this in more detail.
extension Optional {
/// Returns true if the optional is empty
var isNone: Bool {
return self == .none
}
}
Another example would be addign a convenience extension to one of your own enum
types that is fileprivate
so that you'd use it only within a specific file:
fileprivate extension Entity {
mutating func replace(to: Entity) {
self = entity
}
}
Here, we have an extension to your Entity
that allows to replace it with a different entity. This would only be used deep within your game engine which is why the scope is limited to one file.
Generic Enums
Enums can also be defined over generic parameters. You'd use them to
adapt the associated values of an enum. The simplest example comes
straight from the Swift standard library, namely the Optional
type.
You probably mostly use it with optional chaining (?
), if let
,
guard let
, or switch
, but syntactically you can also use Optionals
like so:
let aValue = Optional<Int>.some(5)
let noValue = Optional<Int>.none
if noValue == Optional.none { print(\"No value\") }
This is the direct usage of an Optional without any of the syntactic
sugar that Swift adds in order to make your life a tremendous amount
easier. If you look at the code above, you can probably guess that
internally the Optional
is defined as follows 1:
// Simplified implementation of Swift's Optional
enum MyOptional<T> {
case some(T)
case none
}
What's special here is, that the enum's associated values take the
type of the generic parameter T
, so that optionals can be built for
any kind you wish to return.
Enums can have multiple generic parameters. Take the well-known Either type which is not part of Swift's standard library but implemented in many open source libraries as well as prevalent in other functional programming languages like Haskell or F#. The idea is that instead of just returning a value or no value (née Optional) you'd return either one of two different values.
For example, if you parse user input, the user could enter a name or a number,
in that case the type of Either
would be Either<String, Int>
.
enum Either<T1, T2> {
case left(T1)
case right(T2)
}
Finally, all the type constraints that work on classes and structs in
Swift also work on enums. Here, we have a type Bag
that is either empty
or contains an array of elements. Those elements have to be Equatable
.
enum Bag<T: Sequence> where T.Iterator.Element==Equatable {
case empty
case full(contents: [T)]
}
Recursive / Indirect Types
Indirect types allow
you to define enums where the associated value of a case
is the very
same enum again.
As an example, consider that you want to define a file
system representations with files and folders containing files. If
File and Folder were enum cases, then the Folder case would
need to have an array of File cases as it's associated value
. Since
this is a recursive operation, the compiler has to make special
preparations for it. Quoting from the Swift documentation:
Enums and cases can be marked indirect, which causes the associated value for the enum to be stored indirectly, allowing for recursive data structures to be defined.
So to implement our FileNode enum
, we'd have to write it like
this:
enum FileNode {
case file(name: String)
indirect case folder(name: String, files: [FileNode])
}
The indirect
keyword tells the compiler to handle this enum case
indirectly. You can also add the keyword for the whole enum. As an
example imagine mapping a binary
tree:
indirect enum Tree<Element: Comparable> {
case empty
case node(Tree<Element>,Element,Tree<Element>)
}
This is a very powerful feature that allows you to map complex relationships in a very clean way with an enum.
Custom Data Types
If we neglect associated values
, then the value of an enum can only be
an Integer, Floating Point, String, or Boolean. If you need to support
something else, you can do so by implementing the
ExpressibleByStringLiteral
protocol which allows the type in question to
be serialized to and from String.
As an example, imagine you'd like to store the different screen sizes of iOS devices in an enum:
enum Devices: CGSize {
case iPhone3GS = CGSize(width: 320, height: 480)
case iPhone5 = CGSize(width: 320, height: 568)
case iPhone6 = CGSize(width: 375, height: 667)
case iPhone6Plus = CGSize(width: 414, height: 736)
}
However, this doesn't compile because CGSize
is not a literal
and can't be
used as an enum value. Instead, what you need to do is add a type
extension for the ExpressibleByStringLiteral
protocol.
The protocol
requires us to implement an initializer that receives a String
. Next, we need to
take this String
an convert it into a CGSize
. Not any String
can be a CGSize
.
So if the value is wrong, we will crash with an error as this code will be executed
by Swift during application startup. Our string format for sizes is: width,height
extension CGSize: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
let components = rawValue.split(separator: \",\")
guard components.count == 2,
let width = Int(components[0]),
let height = Int(components[1])
else { return fatalError(\"Invalid Format \(value)\") }
self.init(width: size.width, height: size.height)
}
}
Now, we can write our enum
, with one downside though: The initial
values have to be written as a String, since that's what the enum will
use (remember, we complied with ExpressibleByStringLiteral
, so that the
String can be converted to our CGSize
type.
enum Devices: CGSize {
case iPhone3GS = \"320,480\"
case iPhone5 = \"320,568\"
case iPhone6 = \"375,667\"
case iPhone6Plus = \"414,736\"
}
This, finally, allows us to use our CGSize
enum. Keep in mind that in
order to get the actual CGSize
value, we have to access the rawValue
of the enum.
let a = Devices.iPhone5
let b = a.rawValue
print(\"the phone size string is \(a), width is \(b.width), height is \(b.height)\")
This works, because we explicitly told Swift that a CGSize
can be created
from any String
.
A different option to hook into custom types it the RawRepresentable
protocol, we will tackle this next.
Comparing Enums
Just like need to compare strings ("world" == "hello"
) or numbers you sometimes also need to compare enums. For very simple ones, like the following, this is easy as Swift takes care of it:
enum Toggle {
case on, off
}
Toggle.on == Toggle.off
But what if you have a more complex enum
with associated values
like this one?
enum Character {
case warrior(name: String, level: Int, strength: Int)
case wizard(name: String, magic: Int, spells: [String])
}
If you'd try to compare to instances of Character
Swift would complain. By default, it doesn't know how to compare enum
types that have associated values
. However, you can explicitly tell Swift to just compare all the values of each case
and if they're the same, then the types are equal
. To do that, you'd just add an empty conformance to the Equatable
protocol:
enum Character: Equatable {
case warrior(name: String, level: Int, strength: Int)
case wizard(name: String, magic: Int, spells: [String])
}
Just this one addition Equatable
will allow you to compare your types. This only works if all the values in your cases are also Equatable
. This works in our example as Int
, String
and arrays of String
are Equatable
by default.
If you have a custom type that doesn't conform to Equatable
, the above will not work:
struct Weapon {
let name: String
}
enum Character: Equatable {
case warrior(name: String, level: Int, strength: Int, weapon: Weapon)
case wizard(name: String, magic: Int, spells: [String])
}
In this case, Swift will complain that Character
does not conform to Equatable
. So the solution here is to also conform Weapon
to Equatable
.
If that is not an option, you an always implement a custom Equatable
conformance:
// Not Equatable Stock
struct Stock { ... }
enum Trade {
case buy(stock: Stock, amount: Int)
case sell(stock: Stock, amount: Int)
}
func ==(lhs: Trade, rhs: Trade) -> Bool {
switch (lhs, rhs) {
case let (.buy(stock1, amount1), .buy(stock2, amount2))
where stock1 == stock2 && amount1 == amount2:
return true
case let (.sell(stock1, amount1), .sell(stock2, amount2))
where stock1 == stock2 && amount1 == amount2:
return true
default: return false
}
}
As you can see, we're comparing the two possible enum cases
via a
switch, and only if the cases match (i.e. .buy & .buy) will we compare
the actual associated values.
Custom Initializers
Imagine you'd want to initialize an enum
with custom data. In our example
we have a Device
enum that represents Apple devices and we'd like to
also initialize them with non-standard names. Here's the enum
:
enum Device {
case appleWatch
}
Now if a user accidentally enters iWatch
as their device, we still want to map this
to the correct AppleWatch
case. To do that, we will implement a custom initializer
that sets self
to the correct type:
enum Device {
case appleWatch
init?(term: String) {
if term == \"iWatch\" {
self = .appleWatch
} else {
return nil
}
}
}
In the above example, we used a failable initializer. However, normal initializers work just as well:
enum NumberCategory {
case small
case medium
case big
case huge
init(number n: Int) {
if n < 10000 { self = .small }
else if n < 1000000 { self = .medium }
else if n < 100000000 { self = .big }
else { self = .huge }
}
}
Iterating over Enum Cases
Say you've created a nice new enum with several cases:
enum Drink: String {
case coke, beer, water, soda, lemonade, wine, vodka, gin
}
Now, you'd like to display all of those drinks at runtime in a list. You somehow want to run a for-each
loop over all of your enum cases. The enum
type does not offer this ability out-of-the-box. Instead, you have to explicitly tell the Swift compiler that you wish for your enum to be iterable. You do this by conforming to the empty CaseIterable
protocol:
enum Drink: String, CaseIterable {
case coke, beer, water, soda, lemonade, wine, vodka, gin
}
Now, you can easily iterate over your enum
with the new allCases
property:
for drink in Drink.allCases {
print(\"For lunch I like to drink \(drink)\)\")
}
This works only if your enum
cases do not contain any associated values:
enum Drink: CaseIterable {
case beer
case cocktail(ingredients: [String])
}
This code will not compile and the reason for that is simple. The Swift compiler does not know how to construct the cocktail
case. And for good reason, should it be a Gin Tonic, or a Cuba libre? You wouldn't want the Swift compiler to decide that, but it has to! Because in order for you use allCases
it will need to return an enum
case including associated values.
So there it is, CaseIterable
is a great Swift feature, however keep in mind that it can only be used with simple enum
cases.
Objective-C Support
Integer-based enums such as can be bridged to Objective-c via the @objc
attribute:
@objc enum Bit: Int {
case zero = 0
case one = 1
}
However once you venture away from
integers (say String
) or start using associated values
you can't
use enums from within Objective-C.
There is a manual way though. Add two methods to your
enum
, define a type replacement on the @objc
side, and you can move
enums
back and forth just fine, without having to conform to private
protocols:
enum Trade {
case buy(stock: String, amount: Int)
case sell(stock: String, amount: Int)
}
// This type could also exist in Objective-C code.
@objc class ObjcTrade: NSObject {
var type: Int
var stock: String
var amount: Int
init(type: Int, stock: String, amount: Int) {
self.type = type
self.stock = stock
self.amount = amount
}
}
extension Trade {
func toObjc() -> ObjcTrade {
switch self {
case let .buy(stock, amount):
return ObjcTrade(type: 0, stock: stock, amount: amount)
case let .sell(stock, amount):
return ObjcTrade(type: 1, stock: stock, amount: amount)
}
}
static func fromObjc(source: ObjcTrade) -> Trade? {
switch (source.type) {
case 0: return Trade.buy(stock: source.stock, amount: source.amount)
case 1: return Trade.sell(stock: source.stock, amount: source.amount)
default: return nil
}
}
}
This still has the downside that you need to mirror your enum
via an
NSObject
based type on the Objective-C side (or you could just go and
use an NSDictionary
), but if you ever end up in a situation where you
need to access an enum with associated values from Objective-C, this
is a way to do it.
Enums in the Standard Library
Before we go on and explore various use cases for enums in your projects, it might be tempting to see some of the enums being used in the Swift standard library, so let's have a look.
Bit
The Bit
enum can have two possible values, One, and Zero. It
is used as the Index
type for CollectionOfOne<T>
.
FloatingPointClassification
This enum defines the set of possible IEEE 754 "classes", like
NegativeInfinity
, PositiveZero
, or SignalingNaN
.
Mirror.AncestorRepresentation, and Mirror.DisplayStyle These two are used in the context of the Swift Reflection API.
Optional Not much to say here
Process
The Process enum contains the command line arguments of the current
process (Process.argc
, Process.arguments
). This is a particularly
interesting enum
as it used to be a struct
in Swift 1.0.
Practical Usecases
We've already seen a couple of useful enums
in the previous feature
descriptions. Examples would be Optional
, Either
,
FileNode
, or the binary tree. However, there're many more scenarios
where using an enum
wins over a struct
or class
. Usually, if your
problem domain can be divided into a finite set of distinctive
categories, an enum
may be the right choice. Even only two cases are a
perfectly valid scenario for an enum, as the Optional and Either types
show.
Here, then, are some more examples of practical enum
usage to fuel
your creativity.
Error Handling
One of the prime examples of Enum usage in Swift is, of course, the new
error handling in Swift 2.0. Your throwing function can throw anything
which conforms to the empty ErrorType
protocol. As the Swift
documentation succinctly observes:
Swift enumerations are particularly well suited to modeling a group of related error conditions, with associated values allowing for additional information about the nature of an error to be communicated.
As an example, have a look at the popular JSON Decoding library Argo. When their JSON Decoding fails, it can fail due to two primary reasons.
- The JSON Data lacks a key which the end model requires (say your
model has a property
username
and somehow the JSON lacks that) - There's a type mismatch. Say instead of a String the
username
property in the JSON contains anNSNull
1.
In addition to that, Argo also includes a custom error for anything not
fitting in these two categories above. Their ErrorType enum
looks like
this:
enum DecodeError: Error {
case typeMismatch(expected: String, actual: String)
case missingKey(String)
case custom(String)
}
All cases have associated values that contain additional information about the error in question.
A more general ErrorType
for complete HTTP / REST API handling could
look like this:
enum APIError : Error {
// Can't connect to the server (maybe offline?)
case connectionError(error: NSError)
// The server responded with a non 200 status code
case serverError(statusCode: Int, error: NSError)
// We got no data (0 bytes) back from the server
case noDataError
// The server response can't be converted from JSON to a Dictionary
case JSONSerializationError(error: Error)
// The Argo decoding Failed
case JSONMappingError(converstionError: DecodeError)
}
This ErrorType
implements the complete REST Stack up to the point
where your app would get the completely decoded native struct
or
class
object.
More information on ErrorType
and more enum
examples in this context
can be found in the official documentation
here.
Observer Pattern
There're various ways of modelling observation in Swift. If you include
@objc
compatibility, you can use NotificationCenter
or KVO.
Even if not, the didSet
syntax makes it easy to implement simple
observation. Enums can be used here in order to make the type of change
that happens to the observed object clearer. Imagine collection
observation. If we think about it, we only have a couple of possible
cases: One or more items are inserted, one or more items are deleted,
one or more items are updated. This sounds like a job for an enum:
enum Change {
case insertion(items: [Item])
case deletion(items: [Item])
case update(items: [Item])
}
Then, the observing object can receive the concrete information of what happened in a very clean way. This could easily be extended by adding oldValue and newValue, too.
Status Codes
If you're working with an outside system which uses status codes (or error codes) to convey information, like HTTP Status Codes, enums are obviously a great way to encode the information. 1
enum HttpError: String {
case code400 = \"Bad Request\"
case code401 = \"Unauthorized\"
case code402 = \"Payment Required\"
case code403 = \"Forbidden\"
case code404 = \"Not Found\"
}
Result Types
Enums are also frequently used to map the result of JSON parsing into the Swift type system. Here's a short example of this:
enum JSON {
case JSONString(Swift.String)
case JSONNumber(Double)
case JSONObject([String : JSONValue])
case JSONArray([JSONValue])
case JSONBool(Bool)
case JSONNull
}
Similarly, if you're parsing something else, you may use the very same
structure to convert your parsing results into Swift types. This also
makes perfect sense to only do it during the parsing / processing step
and then taking the JSON enum
representation and converting it into
one of your application's internal class
or struct
types.
UIKit Identifiers
Enums can be used to map reuse identifiers or storyboard identifiers from stringly typed information to something the type checker can understand. Imagine a UITableView with different prototype cells:
enum CellType: String {
case buttonValueCell = \"ButtonValueCell\"
case unitEditCell = \"UnitEditCell\"
case labelCell = \"LabelCell\"
case resultLabelCell = \"ResultLabelCell\"
}
Units
Units and unit conversion are another nice use case for enums. You can map the units and their respective values and then add methods to do automatic conversions. Here's an oversimplified example.
enum Liquid: Float {
case ml = 1.0
case l = 1000.0
func convert(amount: Float, to: Liquid) -> Float {
if self.rawValue < to.rawValue {
return (self.rawValue / to.rawValue) * amount
} else {
return (self.rawValue * to.rawValue) * amount
}
}
}
Another example of this would be Currency conversion. Also, mathematical symbols (such as degrees vs radians) can benefit from this.
Games
Enums are a great use case for games, where many entities on screen
belong to a specific family of items (enemies, obstacles, textures,
...). In comparison to native iOS or Mac apps, games oftentimes are a
tabula rasa. Meaning you invent a new world with new relationships and
new kinds of objects, whereas on iOS or OSX you're using a well-defined
world of UIButtons
, UITableViews
, UITableViewCells
or NSStackView
.
What's more, since Enums can conform to protocols, you can utilize protocol extensions and protocol based programming to add functionality to the various enums that you defined for your game. Here's a short example that tries to display such a hierarchy:
enum FlyingBeast { case dragon, hippogriff, gargoyle }
enum Horde { case ork, troll }
enum Player { case mage, warrior, barbarian }
enum NPC { case vendor, blacksmith }
enum Element { case tree, fence, stone }
protocol Hurtable {}
protocol Killable {}
protocol Flying {}
protocol Attacking {}
protocol Obstacle {}
extension FlyingBeast: Hurtable, Killable, Flying, Attacking {}
extension Horde: Hurtable, Killable, Attacking {}
extension Player: Hurtable, Obstacle {}
extension NPC: Hurtable {}
extension Element: Obstacle {}
Stringly Typed Code
In bigger Xcode projects, you're quickly accumulating lots of resources
which are accessed by string. We've already mentioned reuse identifiers
and storyboard identifiers above, but there's also: Images, Segues,
Nibs, Fonts, and other resources. Oftentimes, those resources can be
grouped into several distinct sets. If that's the case, a String
typed enum
is a good way of having the compiler check this for you.
enum DetailViewImages: String {
case background = \"bg1.png\"
case sidebar = \"sbg.png\"
case actionButton1 = \"btn1_1.png\"
case actionButton2 = \"btn2_1.png\"
}
For iOS users, there's also R.swift which auto generates structs
for
most of those use cases.
Sometimes you may need more control though (or you may be on a Mac 1)
API Endpoints
Rest APIs are a great use case for enums. They're naturally grouped, they're limited to a finite set of APIs, and they may have additional query or named parameters which can be modelled through associated values.
Take, for example, a look at a simplified version of the Instagram API
enum Instagram {
enum Media {
case popular
case shortcode(id: String)
case search(lat: Float,
min_timestamp: Int,
lng: Float,
max_timestamp: Int,
distance: Int)
}
enum Users {
case user(id: String)
case feed
case recent(id: String)
}
}
Ash Furrow's Moya library is based
around this idea of using enums
to map rest endpoints.
Linked Lists
Airspeed Velocity has a great writeup on how to implement a Linked List
with an enum
. Most of the
code in his post goes far beyond enums and touches a lot of interesting
topics 1, but the basis of his linked list looks kinda like this (I
simplified it a bit):
enum List {
case end
indirect case node(Int, next: List)
}
Each node case
points to the next case, and by using an enum
instead
of something else, you don't have to use an optional for the next
value to signify the termination of the list.
Airspeed Velocity also wrote a great post about the implementation of a red black tree with indirect Swift enums, so while you're already reading his blog, you may just as well also read this one.
Settings
This is a very, very smart solution that Erica Sadun came up
with.
Basically whenever you'd use a dictionary of attribute keys as a way to
configure an item, you'd instead use a Set
of enums with associated
values. That way, the type checker can confirm that your configuration
values are of the correct type.
For more details, and proper examples, check out her original blog post.
Limitations
Tuples
The biggest issue is, Tuple support. I love tuples, they make many things easier, but they're currently under-documented and cannot be used in many scenarios. In terms of enums, you can't have tuples as the enum value:
enum Devices: (intro: Int, name: String) {
case iPhone = (intro: 2007, name: \"iPhone\")
case appleTV = (intro: 2006, name: \"Apple TV\")
case appleWatch = (intro: 2014, name: \"Apple Watch\")
}
This may not look like the best example, but once you start using enums, you'll often end up in situations where you'd like to be able to do something like the above.
Default Associated Values
Another thing which you may run into is that associated values are always types but you can't set a default value for those types. Imagine such an example:
enum Characters {
case mage(health: Int = 70, magic: Int = 100, strength: Int = 30)
case warrior(health: Int = 100, magic: Int = 0, strength: Int = 100)
case neophyte(health: Int = 50, magic: Int = 20, strength: Int = 80)
}
You could still create new cases with different values, but the default settings for your character would be mapped.
Similar Articles |
---|