Skip to content

Optional Chaining

Optional chaining lets you access properties, methods, or subscripts on an optional value that might be nil. If the optional holds a value, the access succeeds; if it's nil, it returns nil without crashing. You can chain multiple accesses, and the whole chain fails safely if any part is nil.

Note: This is like sending messages to nil in Objective-C, but it works with any type and allows checking for success.

Using Optional Chaining Instead of Forced Unwrapping

Add a question mark (?) after an optional to chain access, similar to using an exclamation mark (!) for forced unwrapping. The key difference: chaining returns nil gracefully if the optional is nil, while forced unwrapping causes a runtime error.

The result of optional chaining is always optional, even if the underlying value isn't. For example, accessing an Int property via chaining gives Int?. Use this to check if the access worked (non-nil means success).

Here's an example with two classes: Employee and Home.

swift
class Employee {
    var home: Home?
}

class Home {
    var roomCount = 1
}

Create an Employee instance; its home starts as nil.

swift
let alex = Employee()

Forced unwrapping fails if home is nil:

swift
// let totalRooms = alex.home!.roomCount  // Runtime error!

With optional chaining:

swift
if let totalRooms = alex.home?.roomCount {
    print("Alex's home has \(totalRooms) rooms.")
} else {
    print("Couldn't get the room count.")
}
// Prints "Couldn't get the room count."

This chains to roomCount only if home exists. Since it's nil, you get nil, and optional binding handles it.

Even though roomCount is a plain Int, chaining makes the result Int?.

Assign a Home to make it work:

swift
alex.home = Home()

if let totalRooms = alex.home?.roomCount {
    print("Alex's home has \(totalRooms) rooms.")
} else {
    print("Couldn't get the room count.")
}
// Prints "Alex's home has 1 rooms."

Creating Classes for Multi-Level Chaining

Optional chaining shines with nested optionals. Let's expand the model with Bedroom and Location classes.

The Employee class stays simple:

swift
class Employee {
    var home: Home?
}

Home now has an array of bedrooms, a computed room count, a subscript for access, a method to print the count, and an optional location:

swift
class Home {
    var bedrooms: [Bedroom] = []
    var roomCount: Int {
        return bedrooms.count
    }
    subscript(index: Int) -> Bedroom {
        get {
            return bedrooms[index]
        }
        set {
            bedrooms[index] = newValue
        }
    }
    func displayRoomCount() {
        print("There are \(roomCount) rooms.")
    }
    var location: Location?
}

Bedroom has a name:

swift
class Bedroom {
    let name: String
    init(name: String) { self.name = name }
}

Location has optional building details and a method to build an identifier:

swift
class Location {
    var siteName: String?
    var siteNumber: String?
    var road: String?
    func siteIdentifier() -> String? {
        if let siteNumber = siteNumber, let road = road {
            return "\(siteNumber) \(road)"
        } else if siteName != nil {
            return siteName
        } else {
            return nil
        }
    }
}

Accessing Properties with Optional Chaining

Try accessing roomCount on a new Employee:

swift
let alex = Employee()
if let totalRooms = alex.home?.roomCount {
    print("Alex's home has \(totalRooms) rooms.")
} else {
    print("Couldn't get the room count.")
}
// Prints "Couldn't get the room count."

You can also set properties via chaining, but it fails silently if any link is nil:

swift
let newLocation = Location()
newLocation.siteNumber = "42"
newLocation.road = "Maple Lane"
alex.home?.location = newLocation  // Fails because home is nil

To see evaluation short-circuiting, use a function:

swift
func makeLocation() -> Location {
    print("Function called.")
    let loc = Location()
    loc.siteNumber = "42"
    loc.road = "Maple Lane"
    return loc
}
alex.home?.location = makeLocation()  // Function not called

Calling Methods with Optional Chaining

Chain methods too, even void ones (they return Void?):

swift
if alex.home?.displayRoomCount() != nil {
    print("Displayed room count successfully.")
} else {
    print("Couldn't display room count.")
}
// Prints "Couldn't display room count."

Setting properties returns Void? for success checks.

Accessing Subscripts with Optional Chaining

Place ? before subscript brackets for optional bases:

swift
if let firstBedroomName = alex.home?[0].name {
    print("First bedroom: \(firstBedroomName).")
} else {
    print("Couldn't get first bedroom name.")
}
// Prints "Couldn't get first bedroom name."

Add data to succeed:

swift
let alexHome = Home()
alexHome.bedrooms.append(Bedroom(name: "Main Bedroom"))
alexHome.bedrooms.append(Bedroom(name: "Guest Room"))
alex.home = alexHome

if let firstBedroomName = alex.home?[0].name {
    print("First bedroom: \(firstBedroomName).")
} else {
    print("Couldn't get first bedroom name.")
}
// Prints "First bedroom: Main Bedroom."

For optional subscript results (like dictionary values), add ? after brackets:

swift
var grades = ["Sam": [85, 90, 88], "Lee": [78, 92, 80]]
grades["Sam"]?[0] = 95
grades["Lee"]?[0] += 1
grades["Pat"]?[0] = 70
// "Sam" now [95, 90, 88]; "Lee" [79, 92, 80]; "Pat" unchanged

Multi-Level Chaining

Chain deeply; return type gains at most one optional layer.

Access nested property:

swift
if let alexRoad = alex.home?.location?.road {
    print("Alex's road: \(alexRoad).")
} else {
    print("Couldn't get the road.")
}
// Prints "Couldn't get the road."

Set values and retry:

swift
let alexLoc = Location()
alexLoc.siteName = "Green Oaks"
alexLoc.road = "Pine Avenue"
alex.home?.location = alexLoc

if let alexRoad = alex.home?.location?.road {
    print("Alex's road: \(alexRoad).")
} else {
    print("Couldn't get the road.")
}
// Prints "Alex's road: Pine Avenue."

Chaining Methods with Optional Returns

Call methods returning optionals and chain further (place ? after parentheses):

swift
if let siteID = alex.home?.location?.siteIdentifier() {
    print("Site ID: \(siteID).")
}
// Prints "Site ID: Green Oaks."

Further chain:

swift
if let startsWithGreen = alex.home?.location?.siteIdentifier()?.hasPrefix("Green") {
    if startsWithGreen {
        print("Site ID starts with 'Green'.")
    } else {
        print("Site ID doesn't start with 'Green'.")
    }
}
// Prints "Site ID starts with 'Green'."

Released under the MIT License.