Skip to main content

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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

  1. Create a function that validates an email address using pattern matching
  2. Implement a simple command parser that extracts commands and arguments from strings
  3. Write a function that processes an array of mixed types (Int, String, Bool) using pattern matching
  4. Build a mini state machine for an e-commerce checkout process with pattern matching
  5. Create a calculator that processes operations using pattern matching on tuples

Additional Resources

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! :)