Swift Learning (10) - Enums (Improved Code Version)
Enums define a common type for a group of related values, allowing you to use these values in a type-safe way within your code.
If you are familiar with C
, you know that enums in C assign related names to a group of integer values. Enums in Swift
are more flexible and do not require a value for every enum case. If you provide a value for an enum case (called a raw value), its type can be a string, character, integer, or floating-point number.
Additionally, enum cases can specify associated values of any type to be stored along with the case, similar to unions and variants in other languages. You can define a set of related enum cases in a single enum, and each case can have associated values of appropriate types.
In Swift
, enum types are first-class types. They adopt many features traditionally supported only by classes, such as computed properties (to provide additional information about the enum value), instance methods (to provide functionality related to the enum value), initializers (to provide an initial value), extensions (to expand their functionality), and protocol conformance (to provide standard functionality).
Enum Syntax
Use the enum
keyword to create an enum and place its entire definition within a pair of braces:
enum SomeEnumeration {
// Enum definition goes here
}
// Example using an enum to represent the four compass directions:
enum CompassPoint {
case north
case south
case east
case west
}
The values defined in an enum (such as north, south, east, and west) are called the cases of that enum. You use the case
keyword to define a new enum case.
Note
Unlike C and Objective-C, Swift’s enum cases are not assigned default integer values when they are created. In the CompassPoint example above, north, south, east, and west are not implicitly assigned values of 0, 1, 2, and 3. Instead, these enum cases are fully-fledged values of the explicitly defined CompassPoint type.
Multiple cases can appear on a single line, separated by commas:
enum Planet {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}
Each enum defines a brand new type. Like other types in Swift
, their names (such as CompassPoint and Planet) start with a capital letter. Give enum types singular names rather than plural, for clarity:
var directionToHead = CompassPoint.west
The type of directionToHead
can also be inferred when it is initialized with a value from CompassPoint
. Once directionToHead
is declared as type CompassPoint
, you can use a shorter dot syntax to set it to another value of CompassPoint
:
directionToHead = .east
When the type of directionToHead
is known, you can omit the enum type name when assigning a new value. This makes code more readable when using enum values with explicit types.
print("directionToHead: \(directionToHead).")
---
output: directionToHead: east.
Matching Enum Values with Switch Statements
You can use a switch
statement to match a single enum value:
directionToHead = .south
switch directionToHead {
case .north:
print("Lots of planets have a north.")
case .south:
print("Watch out for penguins.")
case .east:
print("Where the sun rises.")
case .west:
print("Where the skies are blue.")
} // Prints “Watch out for penguins.”
---
output: Watch out for penguins.
This code can be understood as:
“Check the value of directionToHead. If it is .north, print ‘Lots of planets have a north.’ If it is .south, print ‘Watch out for penguins.’” …and so on.
As introduced in Control Flow, when matching an enum value, a switch
statement must be exhaustive. If you omit a case such as .west
, the code above will not compile because it does not consider all members of CompassPoint
. Exhaustiveness ensures that no enum cases are accidentally omitted.
When you do not need to match every enum case, you can provide a default
branch to cover all unhandled cases:
let somePlanet = Planet.earth
switch somePlanet {
case .earth:
print("Mostly harmless.")
default:
print("Not a safe place for humans.")
} // Prints “Mostly harmless.”
---
output: Mostly harmless.
Iterating Over Enum Cases
In some situations, you may want to get a collection containing all the cases of an enum. This can be achieved as follows:
Make the enum conform to the CaseIterable
protocol. Swift
will generate an allCases
property, which represents a collection of all the enum’s cases. Here’s an example:
enum Beverage: CaseIterable {
case coffee, tea, juice
}
let numberOfChoices = Beverage.allCases.count
print("\(numberOfChoices) beverages available.")
---
output: 3 beverages available.
In the example above, you can access the collection of all Beverage
cases via Beverage.allCases
. The usage of allCases
is the same as with any other collection: the elements are instances of the enum type, so in this case, they are Beverage
values. The example counts the total number of enum cases. In the following example, a for-in
loop is used to iterate over all enum cases:
for beverage in Beverage.allCases {
print(beverage)
} // coffee // tea // juice
---
output: coffee
tea
juice
The syntax above indicates that the enum conforms to the CaseIterable
protocol.
Associated Values
The example in the Enum Syntax section showed how to define and categorize enum cases. You can set a constant or variable to Planet.earth
and inspect its value after assignment. However, sometimes it is useful to store other types of values together with the case. This extra information is called an associated value, and you can change the associated value each time you use that enum case in your code.
You can define a Swift
enum to store associated values of any type, and each enum case can have different types of associated values if needed. This feature is similar to discriminated unions, tagged unions, or variants in other languages.
For example, suppose an inventory tracking system needs to use two different types of barcodes to track products. Some products use a one-dimensional UPC barcode format with digits 0 through 9. Each barcode has a digit representing the number system, followed by five digits for the manufacturer code, five digits for the product code, and a final digit as a check digit to verify the code was scanned correctly:
Other products are labeled with a QR code, which can use any ISO 8859-1 character and can encode a string of up to 2953 characters:
This allows the inventory tracking system to store a UPC code as a tuple of four integers, and a QR code as a string of any length.
In Swift
, you define an enum to represent the two types of product barcodes as follows:
enum BarCode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
This code can be understood as:
“Define an enum called BarCode. One case, upc, has associated values of type (Int, Int, Int, Int), and the other case, qrCode, has an associated value of type String.”
This definition does not provide any actual Int
or String
values; it just defines the types of associated values that can be stored when a constant or variable of type BarCode
is set to .upc
or .qrCode
.
You can then create a new barcode of either type, for example:
var productBarCode = BarCode.upc(8, 85909, 51226, 3)
The example above creates a variable called productBarCode
and assigns it the value BarCode.upc
with associated tuple values (8, 85909, 51226, 3).
The same product can be assigned a different type of barcode, for example:
productBarCode = .qrCode("ABCDEFGHIJKLMNOP")
At this point, the original BarCode.upc
and its integer associated values are replaced by the new BarCode.qrCode
and its string associated value. Constants and variables of type BarCode
can store either a .upc
or a .qrCode
(along with their associated values), but only one at a time.
You can use a switch
statement to check the different barcode types, just as you did when matching enum values earlier. However, this time, the associated values can be extracted as part of the switch
statement. You can extract each associated value as a constant (using the let
prefix) or as a variable (using the var
prefix) in the case branch:
switch productBarCode {
case .upc(let numberSystem, let manuFacturer, let product, let check):
print("UPC: \(numberSystem), \(manuFacturer), \(product), \(check).")
case .qrCode(let productCode):
print("QR code: \(productCode).")
} // Prints “QR code: ABCDEFGHIJKLMNOP”
---
output: QR code: ABCDEFGHIJKLMNOP.
If all associated values for an enum case are extracted as constants or all as variables, you can use a shorthand by placing a single let
or var
before the case name:
productBarCode = .upc(8, 86751, 62119, 3)
switch productBarCode {
case let .upc(numberSystem, manuFactor, product, check):
print("UPC: \(numberSystem), \(manuFactor), \(product), \(check).")
case let .qrCode(productCode):
print("QR code: \(productCode)")
} // Prints “UPC: 8, 86751, 62119, 3.”
---
output: UPC: 8, 86751, 62119, 3.
Raw Values
The barcode example in the Associated Values section demonstrated how to declare enum cases that store different types of associated values. As an alternative, enum cases can be pre-populated with default values (called raw values), and these raw values must all be of the same type.
Here is an enum using ASCII codes as raw values:
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
The enum type ASCIIControlCharacter
has a raw value type of Character
, and sets some common ASCII control characters. For more on Character
, see the Strings and Characters section.
Raw values can be strings, characters, or any integer or floating-point type. Each raw value must be unique within the enum declaration.
Note
Raw values and associated values are different. Raw values are pre-filled when you define the enum, like the three ASCII codes above. For a particular enum case, its raw value is always the same. Associated values are set when you create a constant or variable based on an enum case, and can change.
Implicitly Assigned Raw Values
When using integers or strings as raw values, you do not need to explicitly assign a raw value to every enum case; Swift
will assign them automatically.
For example, when using integers as raw values, the implicit values increase by 1 for each case. If the first enum case does not have a raw value, its value is set to 0.
The following enum is a refinement of the earlier Planet
enum, using integer raw values to indicate each planet’s order from the sun:
enum Planets: Int {
case mercury = 1, venus, earth
case mars, jupiter, saturn, uranus, neptune
}
extension Planets: CaseIterable {}
for planet in Planets.allCases {
print("\(planet)'s raw value is \(planet.rawValue).")
}
---
output: mercury's raw value is 1.
venus's raw value is 2.
earth's raw value is 3.
mars's raw value is 4.
jupiter's raw value is 5.
saturn's raw value is 6.
uranus's raw value is 7.
neptune's raw value is 8.
In the example above, Planet.mercury
has an explicit raw value of 1, Planet.venus
has an implicit raw value of 2, and so on.
When using strings as raw values, each case’s implicit raw value is the case’s name.
Here is a refinement of the CompassPoint
enum, using string raw values to represent the direction names:
enum CompassPoints: String {
case north, south, east, west
}
In the example above, CompassPoints.south
has an implicit raw value of south
, and so on.
You can access the raw value of an enum case using its rawValue
property:
extension CompassPoints: CaseIterable {}
for point in CompassPoints.allCases {
print("The point's raw value is \(point.rawValue).")
}
---
output: The point's raw value is north.
The point's raw value is south.
The point's raw value is east.
The point's raw value is west.
Initializing an Enum Instance from a Raw Value
If you define an enum with raw values, you automatically get an initializer that takes a parameter called rawValue
of the raw value’s type and returns an enum case or nil
. You can use this initializer to create a new enum instance.
This example uses the raw value 7 to create the enum case Uranus
:
let possiblePlanet = Planets(rawValue: 7) // possiblePlanet is of type Planets?, value is Planets.uranus
print("\(possiblePlanet!)'s raw value is \(possiblePlanet!.rawValue).")
---
output: uranus's raw value is 7.
However, not every Int
value will find a matching planet. Therefore, the raw value initializer always returns an optional enum case. In the example above, possiblePlanet
is of type Planets?
, or “optional Planets”.
Note
The raw value initializer is a failable initializer, because not every raw value has a corresponding enum case.
If you try to look up a planet at position 11, the raw value initializer will return nil
:
let positionToFind = 11
if let somePlanet = Planets(rawValue: positionToFind) {
switch somePlanet {
case .earth:
print("Mostly harmless.")
default:
print("Not a safe place for humans.")
}
} else {
print("There isn't a planet at position \(positionToFind).")
} // Prints “There isn't a planet at position 11.”
---
output: There isn't a planet at position 11.
This example uses optional binding to try to access the planet at raw value 11. The statement if let somePlanet = Planets(rawValue: 11)
creates an optional Planets
; if the value exists, it is assigned to somePlanet
. In this example, there is no planet at position 11, so the else
branch is executed.
Recursive Enums
A recursive enum is an enum that has one or more cases that use instances of the enum as associated values. When using recursive enums, the compiler inserts an extra layer of indirection. You can mark a case as recursive by prefixing it with the indirect
keyword.
For example, the following enum stores simple arithmetic expressions:
enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}
You can also prefix the entire enum with the indirect
keyword to indicate that all its cases are recursive:
indirect enum ArithmeticExpressions {
case number(Int)
case addition(ArithmeticExpressions, ArithmeticExpressions)
case multiplication(ArithmeticExpressions, ArithmeticExpressions)
}
The enum defined above can store three kinds of arithmetic expressions: a pure number, the sum of two expressions, and the product of two expressions. The addition
and multiplication
cases have associated values that are also arithmetic expressions—these associated values make nested expressions possible. For example, the expression (5 + 4) * 2 has a number on the right of the multiplication, and an expression on the left. Because the data is nested, the enum type used to store the data must also support nesting—meaning the enum must be recursive. The following code shows how to use the ArithmeticExpression
recursive enum to create the expression (5 + 4) * 2:
let five = ArithmeticExpressions.number(5)
let four = ArithmeticExpressions.number(4)
let sum = ArithmeticExpressions.addition(five, four)
let product = ArithmeticExpressions.multiplication(sum, ArithmeticExpressions.number(2))
To operate on data structures with recursive nature, using a recursive function is a straightforward approach. For example, here is a function to evaluate an arithmetic expression:
func evaluate(_ expression: ArithmeticExpressions) -> Int {
switch expression {
case let .number(value):
return value
case let .addition(left, right):
return evaluate(left) + evaluate(right)
case let .multiplication(left, right):
return evaluate(left) * evaluate(right)
}
}
print(evaluate(product)) // Prints “18”
---
output: 18