Swift Optional Chaining
Optional Chaining is a process for querying and calling properties, methods, and subscripts where the target of the query or call may be nil.
Optional Chaining returns two values:
- If the target has a value, the call succeeds and returns that value.
- If the target is nil, the call returns nil.
Multiple queries or calls can be chained together, and if any link in the chain is nil, the entire chain fails.
Optional Chaining as an Alternative to Forced Unwrapping
By placing a question mark (?) after the optional value of a property, method, or subscript, you define an optional chain.
| Optional Chaining '?' | Exclamation Mark (!) Forced Unwrapping of Methods, Properties, Subscripts | | ? Placed after an optional value to call methods, properties, subscripts | ! Placed after an optional value to forcefully unwrap methods, properties, subscripts | | Friendly error message when the optional is nil | Runtime error when the optional is nil |
Example of Using Exclamation Mark (!) for Optional Chaining
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
let john = Person()
// This will result in a runtime error
let roomCount = john.residence!.numberOfRooms
The above program execution output is:
fatal error: unexpectedly found nil while unwrapping an Optional value
Attempting to forcefully unwrap the residence
property to access numberOfRooms
will cause a runtime error because there is no residence
value to unwrap.
Example of Using Question Mark (?) for Optional Chaining
class Person {
var residence: Residence?
}
class Residence {
var numberOfRooms = 1
}
let john = Person()
// Chaining the optional `residence?` property, if `residence` exists, retrieve `numberOfRooms`
if let roomCount = john.residence?.numberOfRooms {
print("John's room number is \(roomCount).")
} else {
print("Cannot retrieve room number.")
}
The above program execution output is:
Cannot retrieve room number.
Since the attempt to access numberOfRooms
might fail, the optional chaining returns a value of type Int?
, or "optional Int". When residence
is nil (as in the example), the optional Int
will be nil, and thus numberOfRooms
cannot be accessed.
It's important to note that this holds true even if numberOfRooms
is a non-optional Int
. The act of querying through optional chaining means that numberOfRooms
will always return an Int?
instead of an Int
.
Defining Model Classes for Optional Chaining
You can use optional chaining to call properties, methods, and subscripts through multiple levels. This allows you to access lower-level properties within complex models and check if such properties can be successfully accessed.
Example
Four model classes are defined, including multiple levels of optional chaining:
class Person {
var residence: Residence?
}
// Defines a variable `rooms`, initialized as an empty array of type `Room[]`
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
var address: Address?
}
// Room defines a `name` property and an initializer to set the room name
class Room {
let name: String
init(name: String) { self.name = name }
}
// The final class in the model is called Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if buildingName != nil {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
Calling Methods Through Optional Chaining
You can use optional chaining to call a method on an optional value and check if the method call is successful. Even if the method does not return a value, you can still use optional chaining for this purpose.
class Person {
var residence: Residence?
}
// Defines a variable rooms, which is initialized to an empty array of type Room[]
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
var address: Address?
}
// Room defines a name property and an initializer to set the room name
class Room {
let name: String
init(name: String) { self.name = name }
}
// The final class in the model is called Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}
let john = Person()
if (john.residence?.printNumberOfRooms()) != nil {
print("Output the number of rooms")
} else {
print("Unable to output the number of rooms")
}
The output of the above program is:
Unable to output the number of rooms
Use an if statement to check if the printNumberOfRooms
method can be successfully called: If the method is called successfully through optional chaining, the implicit return value of printNumberOfRooms
will be Void
, otherwise, it will return nil
.
Calling Subscripts Through Optional Chaining
You can use optional chaining to try to retrieve a value from a subscript and check if the subscript call is successful. However, you cannot use optional chaining to set a subscript.
Example 1
class Person {
var residence: Residence?
}
// Defines a variable rooms, which is initialized to an empty array of type Room[]
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
var address: Address?
}
// Room defines a name property and an initializer to set the room name
class Room {
let name: String
init(name: String) { self.name = name }
}
// The final class in the model is called Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
let john = Person()
if let firstRoomName = john.residence?[0].name {
print("The name of the first room is \(firstRoomName).")
} else {
print("Unable to retrieve the room.")
}
The above program execution output is:
Unable to retrieve the room.
In the subscript call with optional chaining, the question mark is placed directly after john.residence
, before the subscript brackets, because john.residence
is the optional value that the optional chain is trying to access.
Example 2
If you assign a Residence
instance to john.residence
, and provide one or more Room
instances in its rooms
array, you can use optional chaining to access the instances in the rooms
array through the Residence
subscript:
class Person {
var residence: Residence?
}
// Defines a variable `rooms` initialized as an empty array of type `Room[]`
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
var address: Address?
}
// Room defines a `name` property and an initializer to set the room name
class Room {
let name: String
init(name: String) { self.name = name }
}
// The final class in the model is called `Address`
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}
let john = Person()
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "Living Room"))
johnsHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnsHouse
let johnsAddress = Address()
johnsAddress.buildingName = "The Larches"
johnsAddress.street = "Laurel Street"
john.residence!.address = johnsAddress
if let johnsStreet = john.residence?.address?.street {
print("John's street is \(johnsStreet).")
} else {
print("Unable to retrieve the address.")
}
The above program execution output is:
John's street is Laurel Street.
Accessing Subscripts Through Optional Chaining
Through optional chaining, we can use subscripts to read or write to an optional value and check if the subscript call is successful.
Example
class Person {
var residence: Residence?
}
// Defines a variable `rooms` initialized as an empty array of type `Room[]`
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
var address: Address?
}
// Room defines a name property and an initializer that sets the room's name
class Room {
let name: String
init(name: String) { self.name = name }
}
// The final class in the model is called Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if let buildingName = buildingName {
return buildingName
} else if let buildingNumber = buildingNumber {
return buildingNumber
} else {
return nil
}
}
}
let john = Person()
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "Living Room"))
johnsHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnsHouse
if let firstRoomName = john.residence?[0].name {
print("The first room name is \(firstRoomName)")
} else {
print("Unable to retrieve the room")
}
The above program execution output is:
The first room name is Living Room
Accessing Subscripts of Optional Type
If a subscript returns a value of optional type, such as a key subscript in Swift's Dictionary, you can chain the subscript's optional return value by placing a question mark after the subscript's closing bracket:
var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0] += 1
testScores["Brian"]?[0] = 72
// the "Dave" array is now [91, 82, 84] and the "Bev" array is now [80, 94, 81]
The example above defines a testScores array, which includes two key-value pairs, mapping a String key to an array of integers.
This example uses optional chaining to set the first element of the "Dave" array to 91, increment the first element of the "Bev" array by 1, and attempt to set the first element of the "Brian" array to 72.
The first two calls succeed because the keys exist. However, the key "Brian" does not exist in the dictionary, so the third call fails.
Chaining on Methods with Optional Return Values
You can chain together multiple levels of optional chaining to drill down to properties, methods, and subscripts deeper within a model. However, multiple levels of optional chaining do not add more levels of optionality to the returned value.
If you try to retrieve an Int value through optional chaining, no matter how many levels of chaining are used, the result is always an Int?. Similarly, if you try to retrieve an Int? value through optional chaining, no matter how many levels of chaining are used, the result is always an Int?.
Example 1
The following example attempts to retrieve the street property of the address inside the residence of john. Here, two levels of optional chaining are used to link the residence and address properties, both of which are optional types:
class Person {
var residence: Residence?
}
// Defines a variable rooms, initialized as an empty array of type Room[]
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
return rooms[i]
}
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
var address: Address?
}
// Room defines a name property and an initializer to set the room's name
class Room {
let name: String
init(name: String) { self.name = name }
}
// The final class in the model is called Address
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}
let john = Person()
if let johnsStreet = john.residence?.address?.street {
print("John's address is \(johnsStreet).")
} else {
print("Unable to retrieve the address.")
}
The above program execution output is:
Unable to retrieve the address.
Example 2
If you set an instance of Address as the value of john.residence.address
and set a real value for the address's street property, you can get the value of this property through multiple optional chaining.
class Person {
var residence: Residence?
}
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
subscript(i: Int) -> Room {
get{
return rooms[i]
}
set {
rooms[i] = newValue
}
}
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
var address: Address?
}
class Room {
let name: String
init(name: String) { self.name = name }
}
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if (buildingName != nil) {
return buildingName
} else if (buildingNumber != nil) {
return buildingNumber
} else {
return nil
}
}
}
let john = Person()
john.residence?[0] = Room(name: "Bathroom")
let johnsHouse = Residence()
johnsHouse.rooms.append(Room(name: "Living Room"))
johnsHouse.rooms.append(Room(name: "Kitchen"))
john.residence = johnsHouse
if let firstRoomName = john.residence?[0].name {
print("The first room is \(firstRoomName)")
} else {
print("Unable to retrieve the room.")
}
The above example output is:
The first room is Living Room
Chaining on Methods with Optional Return Values
We can also call methods that return optional values using optional chaining, and continue chaining on the optional values.
### Example
class Person { var residence: Residence? }
// Defines a variable rooms, which is initialized to an empty array of type Room[] class Residence { var rooms = Room var numberOfRooms: Int { return rooms.count } subscript(i: Int) -> Room { return rooms[i] } func printNumberOfRooms() { print("The number of rooms is (numberOfRooms)") } var address: Address? }
// Room defines a name property and an initializer to set the room's name class Room { let name: String init(name: String) { self.name = name } }
// The final class in the model is called Address class Address { var buildingName: String? var buildingNumber: String? var street: String? func buildingIdentifier() -> String? { if (buildingName != nil) { return buildingName } else if (buildingNumber != nil) { return buildingNumber } else { return nil } } }
let john = Person()
if john.residence?.printNumberOfRooms() != nil { print("Room number specified") } else { print("Room number not specified") }
The output of the above program is:
Room number not specified ```