Swift Programming – Optional Chaining

In this tutorial you’ll learn about optional chaining in Swift. To follow along, it is recommended to have a Mac and the Xcode IDE installed on it. This tutorial makes use of the Xcode playground for compilation of the Swift codes. If you do not have a Mac, you may try using the open-source code editors which support different operating systems such as AtomSublimeText or VSCode.

Optional Chaining

You can use optional chaining on optionals to obtain their values if they exist in a safe way. It works on properties, methods and subscripts.

Optional Chaining as an Alternative to Forced Unwrapping

Recall our lesson Swift Programming – Constants and Variables, we discussed about forced unwrapping. Let’s see optional chaining and forced unwrapping in action to understand the difference.

class Room {
    var number = 1001
}

class Tenant {
    var name: String
    var room: Room?
    
    init(name: String) {
        self.name = name
    }
}

let tenant = Tenant(name: "Patrick")

// optional chaining
if let roomNumber = tenant.room?.number {
    print("\(tenant.name) is staying in room \(roomNumber).")
} else {
    print("No record on tenant \(tenant.name)'s room found.")
}

// forced-unwrapping
tenant.room = Room()
print("\(tenant.name) is staying in room \(tenant.room!.number).")

If we did not instantiated the property room on the instance of Tenant, the forced-unwrapping that we did, would have resulted in a runtime error. However, when we used the optional chaining, our code was executed gracefully even though the tenant‘s room property wasn’t initialized. The syntax for the optional chaining is a question mark (?) just after the optional property whilst for the forced-unwrapping it is an exclamation mark (!).

Defining Model Classes for Optional Chaining

So far we used optional chaining on one level of a property of an optional type, i.e. tenant.room?.number. Let’s try another example to have more than one level of a property of an optional type.

class Residence {
    var address: Address?
    var phone: Int?
}

class Address {
    var streetNumber: String?
    var region: String?
    var city: String?
    var country: String?
    
    func getFullAddress() -> String? {
        if let streetNumber = streetNumber, let region = region, let city = city, let country = country {
            return "\(streetNumber) \(region), \(city), \(country)"
        } else if let region = region, let city = city, let country = country {
            return "\(region), \(city), \(country)"
        } else if let city = city, let country = country {
            return "\(city), \(country)"
        } else if let country = country {
            return "\(country)"
        } else {
            return nil
        }
    }
}

class Employee {
    var name: String
    var title: String
    var mobile: Int
    var residence: Residence?
    
    init(name: String, title: String, mobile: Int) {
        self.name = name
        self.title = title
        self.mobile = mobile
    }
}

In our example above, we defined two classes Address and Employee. The Address class has four optional properties and one method which returns an optional String (String?). The Employee class has one optional property and one initializer to initialize the other three properties.

Accessing Properties Through Optional Chaining

Let’s use optional chaining to access the optionals of our above example.

let employee = Employee(name: "Tara", title: "Sales Representative", mobile: 587654321)

// multiple levels of optional chaining
if let city = employee.residence?.address?.city {
    print("The employee \(employee.name) lives in \(city).")
} else {
    print("No address found for \(employee.name).")
}

let address = Address()
address.city = "Middleton"
address.country = "America"
employee.residence?.address = address

// multiple levels of optional chaining
if let address = employee.residence?.address?.getFullAddress() {
    print("\(employee.name)'s address is \(address)")
} else {
    print("No address found for \(employee.name).")
}

Notice that even though we used optional chaining on the instance employee to set an address to its residence‘s property, we are still not getting that address on our employee‘s record. This is because the property residence is nil and assigning values to its properties will not work. The appropriate way to assign values to the address property of the employee‘s residence, is to create an instance of residence and set an instance of address to it.

let residence = Residence()
residence.address = address
employee.residence = residence

// multiple levels of optional chaining
if let address = employee.residence?.address?.getFullAddress() {
    print("\(employee.name)'s address is \(address)")
} else {
    print("No address found for \(employee.name).")
}

Reference

https://docs.swift.org/swift-book/LanguageGuide/OptionalChaining.html