Enums provide a way to define a common type for a group of related values. Enums create distinct cases for these values. Then you can work with, switch over and iterate through these distinct cases, making your code much more clear.
What are Enumerations?
Think of enums as a way to create your own custom set of options. For example, the days of the week, the suits in a deck of cards, or the different states an application can be in.
Swift enumerations are very flexible. They can have:
- Computed properties
- Instance methods
- Initialisers
- Cases that can specify associated values of any type
- Cases that can define raw values of a set type
Let’s explore these features.
Basic Enumeration Syntax
You define an enumeration with the enum
keyword, followed by its name and a pair of braces containing the cases.
Example: Compass Directions
enum CompassPoint {
case north
case south
case east
case west
}
You can also define multiple cases on a single line, separated by commas:
enum Planet {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}
Once defined, you can use these cases like any other type:
var directionToHead = CompassPoint.west
directionToHead = .east // Swift infers the type once set
Matching Enumeration Values with a Switch Statement
A common way to work with enums is using a switch
statement:
switch directionToHead {
case .north:
print("Brace yourselves, winter is coming.")
case .south:
print("Time to waddle? Watch out for penguins!")
case .east:
print("Here comes the sun, and I say it's all right.")
case .west:
print("Go West! (Life is peaceful there).")
}
// Prints "Here comes the sun, and I say it's all right."
When writing switch
statements for enums, you must cover all possible cases. If you can’t cover all cases, or don’t want to, the compiler will raise an error and you can use default
instead.
Raw Values
Each case in an enum can come with a default value called raw values. These raw values must all be of the same type (e.g., String
, Character
, Int
, Float
). You can define the rawValue
type by adding a data type after the enum declaration.
Example: Barcode Types
enum Barcode: String {
case upc = "UPC"
case qrCode = "QR_CODE"
case ean = "EAN"
}
let productBarcode = Barcode.upc
print("Barcode type: \(productBarcode.rawValue)") // Prints "Barcode type: UPC"
Implicit Raw Values
If you’re using integers or strings for raw values, Swift can automatically assign them.
For integers, if you don’t specify a value for the first case, it defaults to 0, and subsequent cases increment by 1.
enum Order: Int {
case first // rawValue is 0
case second // rawValue is 1
case third // rawValue is 2
}
For strings, if you don’t specify a raw value, it defaults to the name of the case itself.
enum FileType: String {
case swift // rawValue is "swift"
case markdown // rawValue is "markdown"
case json // rawValue is "json"
}
You can also initialise an enum from a raw value, which returns an optional enum case (as the raw value might not correspond to a valid case):
if let someOrder = Order(rawValue: 1) {
print("This is the \(someOrder) order.") // Prints "This is the second order."
}
Associated Values
One of Swift’s most powerful enum features is associated values. This allows you to store additional custom information alongside each case value. The type of associated value can be different for each case.
Example: Server Response
enum ServerResponse {
case success(data: String)
case failure(errorCode: Int, errorMessage: String)
case loading
}
func handle(response: ServerResponse) {
switch response {
case .success(let data):
print("Received data: \(data)")
case .failure(let code, let message):
print("Error \(code): \(message)")
case .loading:
print("Still loading...")
}
}
handle(response: .success(data: "{"user": "Mike"}"))
handle(response: .failure(errorCode: 404, errorMessage: "Not Found"))
Associated values let you model complex states or data structures in a very clean and expressive way.
Iterating Over Enumeration Cases
For enumerations that don’t have associated values, you can enable iteration over all cases by conforming to the CaseIterable
protocol. Swift then automatically provides an allCases
collection.
Example: Days of the Week
enum DayOfWeek: String, CaseIterable {
case monday, tuesday, wednesday, thursday, friday, saturday, sunday
}
for day in DayOfWeek.allCases {
print("It's \(day.rawValue)!")
}
let numberOfDays = DayOfWeek.allCases.count
print("There are \(numberOfDays) days in the week.") // Prints "There are 7 days in the week."
Methods and Computed Properties
Beyond just defining cases, Swift enumerations can encapsulate logic related to those cases by including methods and computed properties. This makes enums powerful tools for modeling states and behaviours. Let’s look at a more practical example: managing the state of a document in a workflow.
Example: Document Workflow State
Imagine we have a document that can go through several stages: draft
, inReview
, approved
, or rejected
. We can model this with an enum, and add logic to manage these states.
enum DocumentState {
case draft
case inReview(reviewer: String) // Document is with a specific reviewer
case approved(approver: String, version: Double) // Approved by someone, with a version
case rejected(reason: String, by: String) // Rejected for a reason, by someone
// Computed Property: Determines if the document can be edited
var isEditable: Bool {
switch self {
case .draft, .rejected:
// Drafts and rejected documents can typically be edited
return true
case .inReview, .approved:
// Documents in review or already approved are usually locked for edits
return false
}
}
// Computed Property: Identifies who is currently responsible for the document
var currentResponsibleParty: String? {
switch self {
case .draft:
return "Author" // Or could be an associated value if authors change
case .inReview(let reviewer):
return reviewer
case .approved(let approver, _): // We only need the approver here
return approver
case .rejected(_, let rejector): // We only need who rejected it
return rejector
}
}
// Computed Property: Provides a human-readable summary of the current state
var summary: String {
switch self {
case .draft:
return "Status: Draft. Document is currently being written or revised."
case .inReview(let reviewer):
return "Status: In Review. Awaiting feedback from \(reviewer)."
case .approved(let approver, let version):
return "Status: Approved by \(approver) as Version \(String(format: "%.1f", version))."
case .rejected(let reason, let by):
return "Status: Rejected by \(by). Reason: \"\(reason)\"."
}
}
// Instance Method: Checks if a transition to a new state is valid
func canTransition(to newState: DocumentState) -> Bool {
// Basic state transition logic
switch (self, newState) {
case (.draft, .inReview):
return true // A draft can be submitted for review
case (.inReview, .approved):
return true // A document in review can be approved
case (.inReview, .rejected):
return true // A document in review can also be rejected
case (.rejected, .draft):
return true // A rejected document can be moved back to draft for revisions
case (.approved, .draft): // Example: Starting a new version from an approved one
return true
default:
// All other transitions are considered invalid by default
return false
}
}
}
Now, let’s demonstrate how to use this DocumentState
enum:
// Let's create a document and move it through the workflow
var myDocument = DocumentState.draft
print(myDocument.summary)) // Prints "Status: Draft. Document is currently being written or revised."
print("Is editable? \(myDocument.isEditable)") // Prints "Is editable? true"
if myDocument.canTransition(to: .inReview(reviewer: "Alice")) {
myDocument = .inReview(reviewer: "Alice")
print(myDocument.summary) // Prints "Status: In Review. Awaiting feedback from Alice."
print("Current responsible: \(myDocument.currentResponsibleParty ?? "N/A")") // Prints "Current responsible: Alice"
}
// Alice approves the document
if myDocument.canTransition(to: .approved(approver: "Alice", version: 1.0)) {
myDocument = .approved(approver: "Alice", version: 1.0)
print(myDocument.summary) // Prints "Status: Approved by Alice as Version 1.0."
print("Is editable? \(myDocument.isEditable)") // Prints "Is editable? false"
}
// Attempt an invalid transition from approved to rejected
// Assuming our canTransition logic doesn't allow approved -> rejected directly
let nextStateAttempt = DocumentState.rejected(reason: "Too short", by: "Bob")
if myDocument.canTransition(to: nextStateAttempt) {
myDocument = nextStateAttempt
print("Transitioned to rejected.") // This won't print based on the current simple logic
} else {
print("Cannot transition from \(myDocument.summary) to \(nextStateAttempt.summary) directly.")
}
This example shows how methods and computed properties add behavior and querying capabilities to your enumerations, making them much more than simple collections of cases.
Typical Use Cases for Enumerations
- State Management: Representing distinct states (e.g.,
loading
,success
,error
). - Configuration: Defining a set of options (e.g.,
SortOrder.ascending
,SortOrder.descending
). - Modeling Discrete Data: Representing things that have a limited set of possibilities (e.g., card suits, user roles, error types).
- Simplifying Complex Logic: Using
switch
statements with enums can make complex conditional logic much easier to read and maintain.
Wrapping Up
Enumerations are a fundamental feature in Swift. They help you write more expressive, type-safe, and maintainable code by allowing you to define a group of related values clearly. Whether you’re using simple cases, raw values, or powerful associated values, enums are an indispensable tool.
Happy coding!