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:
case EnumName.caseName(let value1, let value2, ...)
or
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:
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:
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:
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
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
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:
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:
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.
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:
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:
- Match against enum cases
- Extract associated values in a single step
- Add additional conditions using
where
clauses - Use pattern matching in various contexts like
switch
,if-case
,guard-case
, andfor-case
By using enumeration patterns effectively, you can write more expressive, readable code that's easier to maintain and understand.
Additional Resources
Exercises
-
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. -
Write a function that uses enumeration patterns to filter an array of
Result<Int, Error>
values, extracting only the successful values. -
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! :)