Skip to main content

Swift Enumeration Pattern

Introduction

Swift's enumeration pattern is a powerful feature that allows you to match against enum cases and extract their associated values in a single step. This pattern is especially useful when working with enumerations that have associated values, enabling you to write cleaner, more expressive code that's easier to read and maintain.

In this tutorial, we'll explore how to use enumeration patterns in various Swift pattern matching contexts, focusing on practical applications and real-world examples.

Understanding Enumeration Patterns

An enumeration pattern matches values of enum types. When an enum case has associated values, the enumeration pattern lets you extract those values while matching the case.

Basic Syntax

The basic syntax for an enumeration pattern looks like this:

swift
case EnumName.caseName(let value1, let value2, ...)

or

swift
case .caseName(let value1, let value2, ...)

when the enum type can be inferred from the context.

Using Enumeration Patterns in Switch Statements

The most common use of enumeration patterns is within switch statements. Let's look at an example:

swift
enum Product {
case book(title: String, author: String, price: Double)
case electronic(name: String, price: Double)
case clothing(type: String, size: String, price: Double)
}

let myItem = Product.book(title: "Swift Programming", author: "Apple Inc.", price: 29.99)

switch myItem {
case .book(let title, let author, let price):
print("Book: \"\(title)\" by \(author), $\(price)")
case .electronic(let name, let price):
print("Electronic: \(name), $\(price)")
case .clothing(let type, let size, let price):
print("Clothing: \(type) size \(size), $\(price)")
}

Output:

Book: "Swift Programming" by Apple Inc., $29.99

In this example, the switch statement matches myItem against each enum case. When a match is found (.book in this case), the associated values are extracted into the constants title, author, and price.

Shorthand Syntax for Variable Binding

If you want to extract all associated values as constants or variables, Swift provides a convenient shorthand syntax:

swift
switch myItem {
case let .book(title, author, price):
print("Book: \"\(title)\" by \(author), $\(price)")
case let .electronic(name, price):
print("Electronic: \(name), $\(price)")
case let .clothing(type, size, price):
print("Clothing: \(type) size \(size), $\(price)")
}

This produces the same result as the previous example but with more concise syntax.

Combining Enumeration Patterns with Where Clauses

You can add a where clause to an enumeration pattern to place additional constraints:

swift
let premiumItem = Product.electronic(name: "MacBook Pro", price: 1999.99)

switch premiumItem {
case .book(let title, _, let price) where price > 50:
print("\"\(title)\" is an expensive book")
case .electronic(let name, let price) where price > 1000:
print("\(name) is a premium electronic device")
case .book, .electronic, .clothing:
print("Standard priced item")
}

Output:

MacBook Pro is a premium electronic device

Notice how we used the underscore (_) to ignore the author parameter in the first case because we don't need it.

Using Enumeration Patterns in If, Guard, and While Statements

Enumeration patterns are not limited to switch statements. You can use them in if, guard, and while statements with the case keyword:

If-Case

swift
let product = Product.clothing(type: "T-Shirt", size: "M", price: 19.99)

if case .clothing(let type, let size, _) = product {
print("You have a \(size) size \(type)")
}

Output:

You have a M size T-Shirt

Guard-Case

swift
func processBookOrder(product: Product) {
guard case .book(let title, let author, _) = product else {
print("This function only processes books")
return
}

print("Processing order for \"\(title)\" by \(author)")
}

processBookOrder(product: myItem)
processBookOrder(product: premiumItem)

Output:

Processing order for "Swift Programming" by Apple Inc.
This function only processes books

For-Case

You can also use enumeration patterns in for loops to filter collections:

swift
let inventory: [Product] = [
.book(title: "Swift Programming", author: "Apple Inc.", price: 29.99),
.electronic(name: "Headphones", price: 159.99),
.clothing(type: "Jeans", size: "32", price: 59.99),
.book(title: "Design Patterns", author: "Gang of Four", price: 49.99)
]

print("Books in inventory:")
for case .book(let title, let author, _) in inventory {
print("- \"\(title)\" by \(author)")
}

Output:

Books in inventory:
- "Swift Programming" by Apple Inc.
- "Design Patterns" by Gang of Four

Handling Recursive Enumerations

Swift's enumeration patterns also work well with recursive enumerations. Here's an example using a binary tree:

swift
enum BinaryTree<T> {
case leaf
case node(value: T, left: BinaryTree<T>, right: BinaryTree<T>)
}

let tree = BinaryTree.node(
value: 10,
left: .node(value: 5, left: .leaf, right: .leaf),
right: .node(value: 15, left: .leaf, right: .leaf)
)

func sumValues(in tree: BinaryTree<Int>) -> Int {
switch tree {
case .leaf:
return 0
case .node(let value, let left, let right):
return value + sumValues(in: left) + sumValues(in: right)
}
}

print("Sum of all values in the tree: \(sumValues(in: tree))")

Output:

Sum of all values in the tree: 30

Real-World Example: Handling Network Responses

Let's look at a practical example where enumeration patterns can be very useful: handling network responses.

swift
enum NetworkResult {
case success(data: Data, statusCode: Int)
case failure(error: Error, statusCode: Int?)
case offline(lastUpdated: Date?)
}

func handleNetworkResponse(_ result: NetworkResult) {
switch result {
case .success(let data, let statusCode) where statusCode == 200:
print("Success with \(data.count) bytes of data")

case .success(_, let statusCode):
print("Received status code \(statusCode)")

case .failure(let error, let statusCode) where statusCode == 401:
print("Authentication error: \(error.localizedDescription)")

case .failure(let error, _):
print("Error: \(error.localizedDescription)")

case .offline(let lastUpdated):
if let date = lastUpdated {
print("Device is offline. Last content update: \(date)")
} else {
print("Device is offline. No cached content available.")
}
}
}

// Test our handler
import Foundation

let successResult = NetworkResult.success(
data: "Response data".data(using: .utf8)!,
statusCode: 200
)

let unauthorizedResult = NetworkResult.failure(
error: NSError(domain: "com.example", code: 401, userInfo: [NSLocalizedDescriptionKey: "Unauthorized access"]),
statusCode: 401
)

let offlineResult = NetworkResult.offline(
lastUpdated: Date(timeIntervalSinceNow: -3600)
)

handleNetworkResponse(successResult)
handleNetworkResponse(unauthorizedResult)
handleNetworkResponse(offlineResult)

Output:

Success with 13 bytes of data
Authentication error: Unauthorized access
Device is offline. Last content update: [date string]

Optional Pattern with Enumeration Pattern

You can combine optional patterns with enumeration patterns to handle optionals containing enums:

swift
enum UserRole {
case guest
case member(since: Date)
case admin(level: Int)
}

let currentUser: UserRole? = .member(since: Date(timeIntervalSinceNow: -31536000)) // ~1 year ago

switch currentUser {
case .some(.guest):
print("User is a guest")

case .some(.member(let since)) where since < Date(timeIntervalSinceNow: -31536000 * 2):
print("User is a member for more than 2 years")

case .some(.member):
print("User is a member")

case .some(.admin(let level)) where level > 5:
print("User is a senior admin")

case .some(.admin):
print("User is an admin")

case .none:
print("No user logged in")
}

Output:

User is a member

Summary

Swift's enumeration pattern is a powerful tool for working with enums in your code. It allows you to:

  1. Match against enum cases
  2. Extract associated values in a single step
  3. Add additional conditions using where clauses
  4. Use pattern matching in various contexts like switch, if-case, guard-case, and for-case

By using enumeration patterns effectively, you can write more expressive, readable code that's easier to maintain and understand.

Additional Resources

Exercises

  1. Create an enum Message with cases for text messages, image messages, and location messages, each with appropriate associated values. Then use enumeration patterns to process different types of messages.

  2. Write a function that uses enumeration patterns to filter an array of Result<Int, Error> values, extracting only the successful values.

  3. Create a state machine using enums to represent the states of a traffic light, and use enumeration patterns to handle state transitions.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)