• Skip to main content
  • Skip to footer

Khawer Khaliq

  • Home

Pattern Matching With Optionals in Swift

Share
Tweet
Share
Pin

Since Optional is an enum, we can implement pattern matching with optionals using the enumeration case pattern, which matches a case of an enum type. Swift also provides the optional pattern, which is syntactic sugar for the enumeration case pattern, and makes pattern matching with optionals more intuitive. This article starts by using the enumeration case pattern with an optional, then explores the optional pattern in detail showing how it can be used with conditional statements and loops, how extra conditions can be added, and how we can create more complex pattern matching code involving optionals.

Related article:

  • What Are Swift Optionals and How They Are Used

Contents

Using the enumeration case pattern with optionals
The optional pattern
Optional pattern with if and guard statements
Optional pattern with for-in and while loops
Adding extra conditions
More complex patterns
Conclusion

Using the enumeration case pattern with optionals

Swift implements the Optional type as an enum with two cases, as shown below.

public enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

The two cases of the enum represent the absence and presence of a wrapped value respectively. When there is no wrapped value, the value of the optional is .none. When the optional has a wrapped value, its value is .some, with the wrapped value as the associated value. Since Optional is a generic type, the Wrapped type parameter is used to determine the type of the wrapped value at the time an Optional instance is created.

We can use enumeration case patterns to match the cases of an optional. Although the most common use of enumeration case patterns is in switch statement case labels, they can also be used in the case conditions of if, while, guard, and for-in statements.

Consider the following optional Int to which we assign the value 5. We then use enumeration case patterns with a switch statement to check whether the optional contains a wrapped value, to unwrap and print the value if it exists, and to print a message if it does not.

var possibleInt: Int? = 5

switch possibleInt {
case let .some(int):
    print(int)
case .none:
    print("No int found")
}
// 5

We get the value 5 printed since that is the value wrapped by the optional. Next, we set the optional to nil and see the expected message printed.

possibleInt = nil

switch possibleInt {
case let .some(int):
    print(int)
case .none:
    print("No int found")
}
// No int found

The optional pattern

Swift uses syntactic sugar to make optionals easy to initialize and use. This extends to using the enumeration case pattern with optionals. To make the syntax easier to use, Swift gives us the optional pattern, which matches values wrapped in a some(Wrapped) case of an Optional enum. It lets us replace .some(x) with x?. We can also use nil for the .none case of the optional.

We can rewrite the switch statement we saw in the previous section using the optional pattern. We do it first with an optional Int to which we have assigned the value 5.

var possibleInt: Int? = 5

switch possibleInt {
case let int?:
    print(int)
case nil:
    print("No int found")
}
// 5

Next, we do the same thing with the optional set to nil.

possibleInt = nil

switch possibleInt {
case let int?:
    print(int)
case nil:
    print("No int found")
}
// No int found

Functionally, the optional pattern is identical to the equivalent enumeration case pattern, so it produces exactly the same result. Like most things to do with optionals in Swift, the syntactic sugar makes the code easier to read and understand.

Optional pattern with if and guard statements

Just like the enumeration case pattern, the optional pattern can be used in the case conditions of if and guard statements. The switch statement works naturally with enums that have a number of cases. Since an optional has only two possible cases, and we are usually only looking for one of the two, use of the optional pattern with if and guard statements can lead to more concise code.

To demonstrate this, we assign a value to an optional Int, and use an if statement with the optional pattern to unwrap and print the value.

var possibleInt: Int? = 5

if case let int? = possibleInt {
    print(int)
}
// 5

Next, we set the optional to nil and check for it using an if statement.

possibleInt = nil

if case nil = possibleInt {
    print("No int found")
}
// No int found

The optional pattern can be used with the guard statement in much the same way. Consider the following function.

func printPossibleInt(_ possibleInt: Int?) {
    guard case let int? = possibleInt else {
        print("No int found")
        return
    }
    print(int)
}

We can now call this function with possibleInt as the argument. In the first call, we assign an Int value to the optional while in the second call we set it to nil.

possibleInt = 5
printPossibleInt(possibleInt)   // 5

possibleInt = nil
printPossibleInt(possibleInt)   // No int found

Optional pattern with for-in and while loops

A common use of the optional pattern is with for-in and while loops to enable selective execution of the body of the loop when iterating over a collection of optionals. Here is a simple example of using an optional pattern with a for-in loop.

Consider the following array of optionals with four elements, three of which wrap Int values.

var possibleInts: [Int?] = [4, 3, nil, 1]

If we want to print all the wrapped Int values using a regular for-in loop, for each iteration we have to use an if statement with optional binding to conditionally unwrap the optional.

for possibleInt in possibleInts {
    if let int = possibleInt {
        print(int)
    }
}
// 4
// 3
// 1

Using an optional pattern with a for-in loop automatically does the conditional unwrapping and binding, and the body of the loop executes only for cases where the optional binding succeeds.

for case let int? in possibleInts {
    print(int)
}
// 4
// 3
// 1

If we want the body of the loop to execute only for cases where the optional is nil, we can rewrite the above code as follows.

for case nil in possibleInts {
    print("No int found")
}
// No int found

In this case, the body of the loop executes only once since the array contains one nil.

We can also use the optional pattern with a while loop, as shown below, which continues printing the wrapped Int values until it encounters the first nil, at which point the loop terminates.

var index = 0

while case let int? = possibleInts[index] {
    print(int)
    index += 1
    if index == possibleInts.count {
        break
    }
}
// 4
// 3

As expected, we just get the first two wrapped values printed, since the third element is nil.

Adding extra conditions

We can add extra conditions in all examples of the optional pattern that we have seen in earlier sections.

For a switch statement, extra conditions can be added using a where clause. The switch statement below prints the Int value wrapped by an optional only if the value is greater than 10.

var possibleInt: Int? = 11

switch possibleInt {
case let int? where int > 10:
    print(int)
case _?:
    print("Int found is not greater than 10")
case nil:
    print("No int found")
}
// 11

Note that to make the switch statement exhaustive, we have to add an additional case which matches any wrapped value. In such a case, we just print a message that the value found did not meet the desired condition. We see this in action in the code below when we assign the value 10 to the optional.

possibleInt = 10

switch possibleInt {
case let int? where int > 10:
    print(int)
case _?:
    print("Int found is not greater than 10")
case nil:
    print("No int found")
}
// Int found is not greater than 10

With an if or guard statement that uses an optional pattern, we can add extra conditions by separating them with commas. Here is an if statement with an extra condition.

possibleInt = 11
 
if case let int? = possibleInt, int > 10 {
    print(int)
}
// 11

We test this by assigning the value 10 to the optional. As expected, the code below does not print anything.

possibleInt = 10

if case let int? = possibleInt, int > 10 {
    print(int)
}

The following function provides an example of adding an extra condition to a guard statement which uses an optional pattern.

func printPossibleIntGreaterThan10(_ possibleInt: Int?) {
    guard case let int? = possibleInt, int > 10 else { return }
    print(int)
}

In the code below, we call this function twice, once with an optional with a wrapped value greater than 10, which prints the wrapped value, and then with 10 as the wrapped value, which does not print anything.

possibleInt = 11
printPossibleIntGreaterThan10(possibleInt)   // 11

possibleInt = 10
printPossibleIntGreaterThan10(possibleInt)

Extra conditions can be added to a for-in loop by using a where clause, as shown below.

var possibleInts: [Int?] = [4, 3, nil, 1]

for case let int? in possibleInts where int > 1 {
    print(int)
}
// 4
// 3

Here we get just two iterations of the loop since only two elements of the array meet both the conditions.

With a while loop, just as with if and guard statements, we can add extra conditions by simply separating them with commas.

var index = 0

while case let int? = possibleInts[index], int > 1 {
    print(int)
    index += 1
    if index == possibleInts.count {
        break
    }
}
// 4
// 3

More complex patterns

With pattern matching, we can make our patterns as complex as we need them to be to solve the problem at hand.

Consider the below protocol which represents a pet, with two properties, one representing the name of the pet and the other representing its vaccination status. Note that the name property is an optional since a pet may or may not have a name.

protocol Pet {
    var name: String? { get set }
    var vaccinated: Bool { get set }
}

Next, we define a Dog class which conforms to the Pet protocol. The vaccinated property is set to false and the initializer sets the name property to nil if a name is not provided when calling the initializer.

class Dog: Pet {
    var name: String?
    var vaccinated = false
    
    init(named name: String? = nil) {
        self.name = name
    }
}

Then, we define a Cat class which also conforms to Pet and has an implementation identical to that of Dog.

class Cat: Pet {
    var name: String?
    var vaccinated = false
    
    init(named name: String? = nil) {
        self.name = name
    }
}

Finally, we define a class which represents a person.

class Person {
    var name: String
    var pet: Pet?
    
    init(named name: String) {
        self.name = name
    }
}

Note that unlike Pet, which has an optional name property, the name property of Person is not an optional, which means a person must have a name. The pet property of Person, however, is an optional since a person may or may not have a pet.

Having defined the required types, let’s create an array of persons.

let names = ["Teresa", "John", "Lisa", "Henry", "Roberta"]
let persons = names.map(Person.init)

Next, we assign the persons some pets.

persons[0].pet = Dog(named: "Jasper")
persons[2].pet = Cat(named: "Cuddles")
persons[3].pet = Dog()
persons[4].pet = Dog(named: "Curly")

Here is how the pets have been assigned:

  • Teresa and Roberta have dogs named Jasper and Curly respectively
  • Lisa has a cat named Cuddles
  • Henry has an unnamed dog
  • John does not have any pet

We can use the following code with pattern matching to find out which of the persons have named dogs, and the names of their dogs.

for case let (personName, dogName?) in persons.map({ ($0.name, ($0.pet as? Dog)?.name) }) {
    print("\(personName) has a dog named \(dogName)")
}
// Teresa has a dog named Jasper
// Roberta has a dog named Curly

The following code does the same for the persons with named cats, and the names of their cats.

for case let (personName, catName?) in persons.map({ ($0.name, ($0.pet as? Cat)?.name) }) {
    print("\(personName) has a cat named \(catName)")
}
// Lisa has a cat named Cuddles

Just as we had done in the previous section, we can use a where clause to add extra conditions to our for-in loop. To demonstrate this, we set the value of the boolean vaccinated property of one of the pets to true.

persons[4].pet?.vaccinated = true

Now we can see whose pet has been vaccinated.

for case let (personName, pet?) in persons.map({ ($0.name, $0.pet) }) where pet.vaccinated == true {
    print("\(personName) has a vaccinated pet")
}
// Roberta has a vaccinated pet

We can also use a where clause to find out which person has an unnamed dog.

for case let (personName, dog?) in persons.map({ ($0.name, $0.pet as? Dog) }) where dog.name == nil {
    print("\(personName) has an unnamed dog")
}
// Henry has an unnamed dog

Finally, we find out the name of the person who does not have a pet.

for case let (personName, nil) in persons.map({ ($0.name, $0.pet) }) {
    print("\(personName) does not have a pet")
}
// John does not have a pet

Note that the above result can also be achieved with a for-in loop without an optional pattern but with a where clause, illustrating the fact that in programming there is usually more than one way to get the same result.

for person in persons where person.pet == nil {
    print("\(person.name) does not have a pet")
}
// John does not have a pet

Conclusion

Pattern matching is a powerful feature of Swift. The optional pattern provides a convenient way to implement pattern matching with optionals, using a variety of conditional statements and loops. Where required, extra conditions can be added to make the code more powerful and flexible.

Thank you for reading! I always appreciate constructive comments and feedback. Please feel free to leave a comment in the comments section of this post or start a conversation on X.

Subscribe to get notifications of new posts

No spam. Unsubscribe any time.

Reader Interactions

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Footer

Protocol-Oriented Programming (POP) in Swift

Use protocol-oriented programming to think about abstractions in a completely different way, leveraging retroactive modeling to introduce appropriate abstractions at any point in the development cycle, and creating traits that can let types opt into functionality simply by conforming to a protocol.

Test-Driven Development (TDD) in Swift

Learn how to use Test-Driven Development (TDD) in Swift which not only enables you to write more reliable and maintainable code but also allows refactoring of code in small increments and with greater ease and confidence.

Unwrapping Optionals With Optional Binding in Swift

Learn how to use optional binding to extract the value wrapped by an optional to a constant or variable, as part of a conditional statement or loop, exploring where optional chaining may be used in place of optional binding, and where these techniques can be used together.

Unit Testing and UI Testing in Swift

Learn how to use unit testing to gain confidence in the correctness of code at the unit level, and use UI testing to ensure that the application fulfills user requirements, explained in detail and illustrated using an example application built using SwiftUI.

When and How to Use the Equatable and Identifiable Protocols in Swift

Detailed coverage of the Equatable and Identifiable protocols, and how they can be used to model not only values but also domain entities with identity using Swift value types, to create code that is more efficient, easier to reason about, easily testable and more concurrency-friendly.

The Power of Optional Chaining in Swift

Learn how to use optional chaining to work safely with optionals, to set and retrieve the value of a property of the wrapped instance, set and retrieve a value from a subscript on the wrapped instance, and call a method on the wrapped instance, all without having to unwrap the optional.

What Are Swift Optionals and How They Are Used

This article explains what Swift optionals are, why Swift has them, how they are implemented, and how Swift optionals can be used to better model real-world domains and write safer and more expressive code.

A Protocol-Oriented Approach to Associated Types and Self Requirements in Swift

Use protocol-oriented programming to avoid having to use associated types in many situations but also to effectively use associated types and Self requirements, where appropriate, to leverage their benefits while avoiding the pitfalls.

Encapsulating Domain Data, Logic and Business Rules With Value Types in Swift

Leverage the power of Swift value types to manage domain complexity by creating rich domain-specific value types to encapsulate domain data, logic and business rules, keeping classes lean and focused on maintaining the identity of entities and managing state changes through their life cycles.

Rethinking Design Patterns in Swift – State Pattern

The State pattern, made simpler and more flexible with the power of Swift, with a detailed worked example to illustrate handling of new requirements, also looking at key design and implementation considerations and the benefits and practical applications of the pattern.

Conditional Logic With and Without Conditional Statements in Swift

Shows how to implement conditional logic using conditional statements as well as data structures, types and flow control mechanisms such as loops, balancing simplicity and clarity with flexibility and future-proofing.

Better Generic Types in Swift With the Numeric Protocol

Covers use of the Numeric protocol as a constraint on type parameters of generic types to ensure that certain type parameters can only be used with numeric types, using protocol composition to add relevant functionality as required.

Understanding, Preventing and Handling Errors in Swift

Examines the likely sources of errors in an application, some ways to prevent errors from occurring and how to implement error handling, using the error handling model built into Swift, covering the powerful tools, associated techniques and how to apply them in practice to build robust and resilient applications.

When and How to Use Value and Reference Types in Swift

Explores the semantic differences between value and reference types, some of the defining characteristics of values and key benefits of using value types in Swift, leading into a discussion on how value and reference types play a complementary role in modeling real-world domains and designing applications.

Swift Protocols Don’t Play Nice With Equatable. Or Can They? (Part Two)

Uses type erasure to implement Equatable conformance at the protocol level, allowing us to program to abstractions using protocol types while safely making equality comparisons and using functionality provided by the Swift Standard Library only available to types that conform to Equatable.

Copyright © Khawer Khaliq, 2017-25. All rights reserved.