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
nilin 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.
class Employee {
var home: Home?
}
class Home {
var roomCount = 1
}Create an Employee instance; its home starts as nil.
let alex = Employee()Forced unwrapping fails if home is nil:
// let totalRooms = alex.home!.roomCount // Runtime error!With optional chaining:
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:
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:
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:
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:
class Bedroom {
let name: String
init(name: String) { self.name = name }
}Location has optional building details and a method to build an identifier:
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:
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:
let newLocation = Location()
newLocation.siteNumber = "42"
newLocation.road = "Maple Lane"
alex.home?.location = newLocation // Fails because home is nilTo see evaluation short-circuiting, use a function:
func makeLocation() -> Location {
print("Function called.")
let loc = Location()
loc.siteNumber = "42"
loc.road = "Maple Lane"
return loc
}
alex.home?.location = makeLocation() // Function not calledCalling Methods with Optional Chaining
Chain methods too, even void ones (they return Void?):
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:
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:
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:
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" unchangedMulti-Level Chaining
Chain deeply; return type gains at most one optional layer.
Access nested property:
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:
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):
if let siteID = alex.home?.location?.siteIdentifier() {
print("Site ID: \(siteID).")
}
// Prints "Site ID: Green Oaks."Further chain:
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'."