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!