Swift Pattern Applications
Pattern matching in Swift goes far beyond simple switch
statements. In this tutorial, we'll explore practical applications of Swift's pattern matching capabilities that you can integrate into your everyday coding. By the end of this guide, you'll have a solid understanding of how pattern matching can make your code more readable, maintainable, and elegant.
Introduction to Swift Pattern Applications
Pattern matching in Swift allows you to compare values against patterns rather than using traditional conditional logic. This approach results in cleaner, more declarative code that's easier to read and maintain. While we've covered the basic syntax in previous sections, let's now focus on applying these patterns to solve real programming problems.
Data Extraction and Validation
Extracting Values from Optionals
One of the most common uses of pattern matching is safely extracting values from optionals:
let userInput: String? = "42"
// Using pattern matching to extract the value
if case let .some(value) = userInput {
print("User entered: \(value)")
} else {
print("No input provided")
}
// Output: User entered: 42
Validating User Input
Pattern matching shines when validating user input against specific criteria:
func validatePhoneNumber(_ input: String) {
// Pattern for 10-digit number
if case let input = input, input.count == 10, Int(input) != nil {
print("Valid phone number: \(input)")
} else {
print("Invalid phone number format")
}
}
validatePhoneNumber("1234567890") // Output: Valid phone number: 1234567890
validatePhoneNumber("123-456-7890") // Output: Invalid phone number format
Advanced Control Flow
Early Returns with guard-case
The guard-case
pattern is excellent for validating conditions and exiting early:
func processUserData(_ data: [String: Any]?) {
guard case let .some(userData) = data else {
print("No user data available")
return
}
guard case let name as String = userData["name"] else {
print("Invalid name format")
return
}
print("Processing data for user: \(name)")
}
processUserData(["name": "John", "age": 30])
// Output: Processing data for user: John
processUserData(nil)
// Output: No user data available
processUserData(["age": 30])
// Output: Invalid name format
Handling Multiple Cases
When dealing with multiple possibilities, pattern matching provides elegant solutions:
enum NetworkResponse {
case success(data: Data)
case failure(error: Error)
case timeout
}
func handleNetworkResponse(_ response: NetworkResponse) {
switch response {
case .success(let data) where data.count > 0:
print("Received \(data.count) bytes of data")
case .success:
print("Received empty response")
case .failure(let error) where (error as NSError).code == -1009:
print("No internet connection")
case .failure(let error):
print("Error: \(error.localizedDescription)")
case .timeout:
print("Connection timed out")
}
}
// Example usage
let data = "Hello World".data(using: .utf8)!
handleNetworkResponse(.success(data: data))
// Output: Received 11 bytes of data
let error = NSError(domain: "NetworkError", code: -1009, userInfo: nil)
handleNetworkResponse(.failure(error: error))
// Output: No internet connection
Working with Complex Data Types
Pattern Matching with Arrays
Pattern matching works wonderfully with arrays:
func analyzeResults(_ scores: [Int]) {
switch scores {
case []:
print("No scores available")
case [let score] where score >= 90:
print("Single excellent score: \(score)")
case [_, _, _]:
print("Exactly three scores provided")
case let scores where scores.reduce(0, +) / scores.count >= 85:
print("High average: \(scores.reduce(0, +) / scores.count)")
default:
print("Average score: \(scores.reduce(0, +) / scores.count)")
}
}
analyzeResults([95]) // Output: Single excellent score: 95
analyzeResults([70, 80, 90]) // Output: Exactly three scores provided
analyzeResults([85, 90, 95, 92]) // Output: High average: 90
analyzeResults([70, 75, 80]) // Output: Average score: 75
Pattern Matching with Dictionaries
Matching dictionary patterns can greatly simplify data validation:
func validateUserProfile(_ profile: [String: Any]) {
switch profile {
case let profile where profile["name"] is String && profile["age"] is Int:
print("Valid profile for \(profile["name"]!)")
case let profile where profile["name"] is String:
print("Profile has name but missing or invalid age")
case let profile where profile["age"] is Int:
print("Profile has age but missing or invalid name")
default:
print("Invalid profile")
}
}
validateUserProfile(["name": "Sara", "age": 28])
// Output: Valid profile for Sara
validateUserProfile(["name": "Tim", "age": "twenty-five"])
// Output: Profile has name but missing or invalid age
Real-World Applications
Form Validation
Pattern matching excels at form validation tasks:
struct FormField {
let name: String
let value: Any?
let isRequired: Bool
}
func validateForm(fields: [FormField]) {
for field in fields {
switch field {
case let field where field.isRequired && (field.value == nil || "\(field.value!)".isEmpty):
print("Error: Required field '\(field.name)' is empty")
case let field where field.name == "email" && field.value != nil:
// Simple email validation
let email = "\(field.value!)"
if !email.contains("@") || !email.contains(".") {
print("Error: Invalid email format")
}
case let field where field.name == "age" && field.value != nil:
if let age = field.value as? Int, age < 18 {
print("Warning: User is under 18")
}
default:
continue
}
}
}
let formFields = [
FormField(name: "name", value: "John", isRequired: true),
FormField(name: "email", value: "john@example", isRequired: true),
FormField(name: "age", value: 16, isRequired: false)
]
validateForm(fields: formFields)
// Output:
// Error: Invalid email format
// Warning: User is under 18
Parsing Data Formats
Pattern matching can simplify parsing structured data:
enum JSONValue {
case string(String)
case number(Double)
case object([String: JSONValue])
case array([JSONValue])
case bool(Bool)
case null
}
func describePath(path: [String], in json: JSONValue) {
guard !path.isEmpty else {
print("Empty path")
return
}
var currentJSON = json
var remainingPath = path
while !remainingPath.isEmpty {
let key = remainingPath.removeFirst()
switch currentJSON {
case .object(let dict):
if let value = dict[key] {
currentJSON = value
if remainingPath.isEmpty {
switch value {
case .string(let s): print("Found string: \(s)")
case .number(let n): print("Found number: \(n)")
case .bool(let b): print("Found boolean: \(b)")
case .null: print("Found null")
case .array: print("Found array")
case .object: print("Found object")
}
}
} else {
print("Key '\(key)' not found")
return
}
default:
print("Cannot navigate further: not an object")
return
}
}
}
// Example usage
let sampleJSON: JSONValue = .object([
"user": .object([
"name": .string("John"),
"age": .number(30),
"active": .bool(true),
"friends": .array([
.string("Alice"),
.string("Bob")
])
])
])
describePath(path: ["user", "name"], in: sampleJSON)
// Output: Found string: John
describePath(path: ["user", "active"], in: sampleJSON)
// Output: Found boolean: true
describePath(path: ["user", "location"], in: sampleJSON)
// Output: Key 'location' not found
State Machine Implementation
Pattern matching is perfect for implementing state machines:
enum TrafficLightState {
case red, yellow, green
}
func nextTrafficLightState(current: TrafficLightState, vehiclesWaiting: Bool = false, pedestriansWaiting: Bool = false) -> TrafficLightState {
switch (current, vehiclesWaiting, pedestriansWaiting) {
case (.red, true, _):
print("Changing from red to green - vehicles waiting")
return .green
case (.red, _, _):
print("Staying red - no vehicles waiting")
return .red
case (.green, _, true):
print("Changing from green to yellow - pedestrians waiting")
return .yellow
case (.green, _, _) where Int.random(in: 1...10) > 7:
print("Changing from green to yellow - timed transition")
return .yellow
case (.green, _, _):
print("Staying green")
return .green
case (.yellow, _, _):
print("Changing from yellow to red")
return .red
}
}
var currentState: TrafficLightState = .red
currentState = nextTrafficLightState(current: currentState, vehiclesWaiting: true)
// Output: Changing from red to green - vehicles waiting
currentState = nextTrafficLightState(current: currentState, pedestriansWaiting: true)
// Output: Changing from green to yellow - pedestrians waiting
currentState = nextTrafficLightState(current: currentState)
// Output: Changing from yellow to red
Summary
Swift pattern matching is a versatile feature that can be applied in numerous real-world scenarios:
- Data extraction and validation: Safely unwrap optionals and validate input data
- Control flow management: Create more readable code with sophisticated conditional logic
- Complex data handling: Process arrays, dictionaries, and custom data types with elegance
- Form validation: Validate user input with clear and concise code
- Data parsing: Extract and process structured data efficiently
- State machines: Implement complex state transitions with readable code
By integrating these pattern matching techniques into your Swift code, you'll write more expressive, safer, and more maintainable applications.
Exercises
To practice pattern matching in real-world scenarios, try these exercises:
- Create a function that validates an email address using pattern matching
- Implement a simple command parser that extracts commands and arguments from strings
- Write a function that processes an array of mixed types (Int, String, Bool) using pattern matching
- Build a mini state machine for an e-commerce checkout process with pattern matching
- Create a calculator that processes operations using pattern matching on tuples
Additional Resources
- Swift Documentation on Pattern Matching
- Advanced Swift Pattern Matching
- WWDC sessions on Swift patterns
Pattern matching is a skill that improves with practice. Start integrating these techniques into your everyday coding, and you'll soon find your code becoming more expressive and robust.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)