Skip to main content

Swift Custom Patterns

Pattern matching in Swift is a powerful feature that goes well beyond basic comparisons. While Swift provides several built-in patterns like value binding, tuple patterns, and type casting patterns, you can extend this functionality by creating your own custom patterns. In this tutorial, you'll learn how to create custom patterns that can significantly enhance your code's expressiveness and flexibility.

Introduction to Custom Patterns

Custom patterns allow you to define your own matching logic that can be used in Swift's pattern matching contexts like switch statements and if case expressions. By creating custom patterns, you can:

  • Simplify complex matching conditions
  • Make your code more readable and maintainable
  • Create domain-specific pattern matching rules
  • Abstract away implementation details

Let's dive into how to create and use custom patterns in Swift!

Creating Custom Patterns with Pattern-Matching Operators

Swift allows you to define custom pattern-matching operators that enable you to create your own pattern matching logic. The most common approach is to use the ~= operator, which is what Swift uses behind the scenes for pattern matching.

Basic Custom Pattern

Let's start with a simple example - creating a pattern that checks if a number is within a specific range:

swift
// Custom pattern operator for range matching
func ~= (pattern: ClosedRange<Int>, value: Int) -> Bool {
return pattern.contains(value)
}

let someValue = 15

switch someValue {
case 0...10:
print("Value is between 0 and 10")
case 11...20:
print("Value is between 11 and 20")
default:
print("Value is outside the specified ranges")
}

Output:

Value is between 11 and 20

In this example, we're not actually creating a new pattern syntax - we're just overloading the ~= operator for the ClosedRange<Int> type, which Swift already uses for pattern matching. However, this shows the foundation of custom pattern matching.

Advanced Custom Patterns

Let's create more complex custom patterns that showcase the true power of this feature.

Creating a Divisibility Pattern

Let's create a pattern that checks if a value is divisible by another value:

swift
// Define a structure to represent our custom pattern
struct DivisibleBy {
let divisor: Int

// Define the pattern matching behavior
static func ~= (pattern: DivisibleBy, value: Int) -> Bool {
return value % pattern.divisor == 0
}
}

// Helper function to create our pattern more naturally
func divisibleBy(_ divisor: Int) -> DivisibleBy {
return DivisibleBy(divisor: divisor)
}

// Using our custom pattern
let number = 15

switch number {
case divisibleBy(2):
print("\(number) is even")
case divisibleBy(3):
print("\(number) is divisible by 3")
case divisibleBy(5):
print("\(number) is divisible by 5")
default:
print("\(number) doesn't match our criteria")
}

Output:

15 is divisible by 3

Wait, that's not quite right! The number 15 is divisible by both 3 and 5. The issue is that switch statements in Swift stop at the first matching case. To handle multiple criteria, we'd need a different approach, like checking each condition separately or using a compound pattern.

Creating a Regular Expression Pattern

Let's create a pattern for matching text against regular expressions:

swift
struct RegexPattern {
let pattern: String

static func ~= (regex: RegexPattern, value: String) -> Bool {
guard let range = value.range(of: regex.pattern,
options: .regularExpression,
range: nil,
locale: nil) else {
return false
}

return range.lowerBound == value.startIndex &&
range.upperBound == value.endIndex
}
}

func regex(_ pattern: String) -> RegexPattern {
return RegexPattern(pattern: pattern)
}

// Now we can use regular expressions in our pattern matching
let email = "[email protected]"

switch email {
case regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"):
print("Valid email format")
case regex("^\\d{3}-\\d{3}-\\d{4}$"):
print("This looks like a phone number")
default:
print("Unrecognized format")
}

Output:

Valid email format

This gives us a very expressive way to match strings against regular expressions!

Practical Applications

Domain-Specific Pattern Matching

Custom patterns really shine when they're applied to domain-specific problems. Let's implement a date range matcher for a scheduling application:

swift
struct DatePeriod {
let name: String
let range: ClosedRange<Date>

static func ~= (period: DatePeriod, value: Date) -> Bool {
return period.range.contains(value)
}
}

// Helper function to create date periods
func createDatePeriod(name: String, from: Date, to: Date) -> DatePeriod {
return DatePeriod(name: name, range: from...to)
}

// Let's create some date periods
let now = Date()
let calendar = Calendar.current

let tomorrow = calendar.date(byAdding: .day, value: 1, to: now)!
let nextWeek = calendar.date(byAdding: .day, value: 7, to: now)!
let nextMonth = calendar.date(byAdding: .month, value: 1, to: now)!

let imminent = createDatePeriod(name: "Imminent", from: now, to: tomorrow)
let upcoming = createDatePeriod(name: "Upcoming", from: tomorrow, to: nextWeek)
let future = createDatePeriod(name: "Future", from: nextWeek, to: nextMonth)

// Function to categorize a task by its due date
func categorizeTask(dueDate: Date) -> String {
switch dueDate {
case imminent:
return "This task is due very soon!"
case upcoming:
return "This task is coming up this week."
case future:
return "You have some time for this task."
default:
return "This task is either overdue or very far in the future."
}
}

let taskDueDate = calendar.date(byAdding: .day, value: 3, to: now)!
print(categorizeTask(dueDate: taskDueDate))

Output:

This task is coming up this week.

Custom Pattern for Network Response Handling

Let's create patterns for handling network responses in a more elegant way:

swift
enum NetworkError: Error {
case badRequest
case unauthorized
case notFound
case serverError(code: Int)
case unknown
}

struct HTTPStatusCodePattern {
let range: ClosedRange<Int>

static func ~= (pattern: HTTPStatusCodePattern, value: Int) -> Bool {
return pattern.range.contains(value)
}
}

let successCodes = HTTPStatusCodePattern(range: 200...299)
let clientErrorCodes = HTTPStatusCodePattern(range: 400...499)
let serverErrorCodes = HTTPStatusCodePattern(range: 500...599)

func handleResponse(statusCode: Int) -> Result<String, NetworkError> {
switch statusCode {
case successCodes:
return .success("Request completed successfully")
case 400:
return .failure(.badRequest)
case 401:
return .failure(.unauthorized)
case 404:
return .failure(.notFound)
case serverErrorCodes:
return .failure(.serverError(code: statusCode))
default:
return .failure(.unknown)
}
}

// Test with various status codes
[200, 404, 500].forEach { code in
let result = handleResponse(statusCode: code)

switch result {
case .success(let message):
print("Success: \(message)")
case .failure(let error):
print("Error: \(error)")
}
}

Output:

Success: Request completed successfully
Error: notFound
Error: serverError(code: 500)

Extending Pattern Matching to Custom Types

Let's create a custom pattern for your own types. We'll use a Person struct as an example:

swift
struct Person {
let name: String
let age: Int
}

// Pattern to match people by age group
struct AgeGroup {
let range: ClosedRange<Int>
let label: String

static func ~= (group: AgeGroup, person: Person) -> Bool {
return group.range.contains(person.age)
}
}

let child = AgeGroup(range: 0...12, label: "Child")
let teenager = AgeGroup(range: 13...19, label: "Teenager")
let adult = AgeGroup(range: 20...64, label: "Adult")
let senior = AgeGroup(range: 65...120, label: "Senior")

// Using our custom pattern
let people = [
Person(name: "Emma", age: 8),
Person(name: "Jake", age: 16),
Person(name: "Sarah", age: 35),
Person(name: "Robert", age: 72)
]

for person in people {
switch person {
case child:
print("\(person.name) is a child")
case teenager:
print("\(person.name) is a teenager")
case adult:
print("\(person.name) is an adult")
case senior:
print("\(person.name) is a senior")
default:
print("Age out of expected range")
}
}

Output:

Emma is a child
Jake is a teenager
Sarah is an adult
Robert is a senior

Combining Custom Patterns

One of the powerful features of Swift pattern matching is the ability to combine patterns. Let's create a more complex example combining multiple custom patterns:

swift
// Define some additional patterns for our Person type
struct NameStartsWith {
let prefix: String

static func ~= (pattern: NameStartsWith, person: Person) -> Bool {
return person.name.hasPrefix(pattern.prefix)
}
}

// Combine patterns using functions
func both<T>(_ pattern1: @escaping (T) -> Bool, _ pattern2: @escaping (T) -> Bool) -> (T) -> Bool {
return { value in pattern1(value) && pattern2(value) }
}

// Create a function to use with switch
func matches<T>(_ matchFunction: @escaping (T) -> Bool) -> MatchFunction<T> {
return MatchFunction(matches: matchFunction)
}

struct MatchFunction<T> {
let matches: (T) -> Bool

static func ~= (pattern: MatchFunction<T>, value: T) -> Bool {
return pattern.matches(value)
}
}

// Example usage
let teenagerWithJName = matches<Person> { person in
teenager.range.contains(person.age) && person.name.hasPrefix("J")
}

let examplePerson = Person(name: "Jake", age: 16)

switch examplePerson {
case teenagerWithJName:
print("\(examplePerson.name) is a teenager with a name that starts with J")
default:
print("No special pattern match")
}

Output:

Jake is a teenager with a name that starts with J

When to Use Custom Patterns

Custom patterns are most valuable when:

  1. You find yourself repeating the same pattern matching logic across your codebase
  2. You're working with domain-specific concepts that could benefit from cleaner matching syntax
  3. You want to abstract away complex matching logic to improve code readability
  4. You need to match against computed properties or derived values

Limitations of Custom Patterns

While custom patterns are powerful, they do have some limitations:

  1. They can't extract values like value binding patterns can (though you can combine them with value binding)
  2. Performance overhead from additional function calls (usually negligible)
  3. They might make code harder to understand for developers unfamiliar with the concept

Summary

Custom patterns in Swift provide a powerful way to extend the language's pattern matching capabilities to better suit your specific needs. By overloading the ~= operator, you can create expressive, reusable patterns that make your code more readable and maintainable.

We've covered:

  • How to create basic custom patterns
  • Creating patterns for domain-specific problems
  • Extending pattern matching to custom types
  • Combining multiple patterns together

Custom patterns are an advanced Swift feature that can significantly enhance your code when used appropriately. They allow you to abstract away complex matching logic and create more declarative code that expresses your intent more clearly.

Exercises

  1. Create a custom pattern that matches strings that represent valid hexadecimal color codes (e.g., "#FF5500").
  2. Extend the Person struct to include a birthDate property, and create a custom pattern to match people born in specific decades.
  3. Design a pattern for matching against a time of day (morning, afternoon, evening, night).
  4. Create a pattern that matches arrays containing specific elements.
  5. Implement a custom pattern for validating complex password requirements.

Additional Resources

Keep experimenting with custom patterns to find elegant solutions to complex matching problems in your Swift code!



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