|

Optional Binding: Turn an Optional Into a Value

Molecules of Swift, episode #004

Not every value in Swift is guaranteed to exist. A dictionary lookup may find nothing, and converting a string to a number may fail. That is why these operations often return an optional type, such as Int?. Before using the number, we need to check that it is there.

Swift uses optional binding, written as if let, for this check. It checks whether an optional contains a value and, if it does, creates a new constant with a non-optional type. That constant is available only in the code that runs after the check succeeds.

Getting the number or string is only part of the story. We also need to understand where we can use the new value, how Swift checks several conditions in order, and why a value that exists may still be wrong for the task.

A value we have checked

Let’s convert a string into a number:

let raw = "42"

if let number = Int(raw) {
    print(number + 1) // 43
}

Int(raw) returns Int? because not every string can become a number. If conversion fails, the code inside if does not run. If it succeeds, number has type Int. We can add it to other numbers without another check.

A new value does not change the original

We can give the extracted value another name:

let possibleName: String? = "Mira"

if let name = possibleName {
    print(name.uppercased()) // MIRA
}

name is a new constant available only inside if. The original possibleName still has type String?.

If we do not need a second name, Swift allows a shorter form:

let nickname: String? = "Mira"

if let nickname {
    print(nickname.uppercased()) // MIRA
}

The name looks the same, but inside the block nickname means a new constant of type String. After the block, it refers to the original value of type String? again. Using the same name does not mean that we changed the original variable.

Several checks in one if

Imagine a function that receives two strings: the start and end of a range. We need to convert both to numbers and check that the start is not greater than the end.

func range(from lower: String, to upper: String) -> ClosedRange<Int>? {
    if let start = Int(lower),
       let end = Int(upper),
       start <= end {
        return start...end
    }
    return nil
}

This if contains three checks separated by commas. Swift runs them from left to right and stops as soon as one fails. It reaches start <= end only if both strings were converted to numbers.

A later check can also use values created by earlier checks. We do not need to put several if blocks inside each other.

A value exists, but is it useful?

if let checks only whether a value exists. It does not know the rules of our application.

let count: Int? = 0
let label: String? = ""

if let count { print(count) }       // 0
if let label { print(label.count) } // 0

Both blocks run: zero is a number, and an empty string is still a string. If we need a nonempty string, we must check that separately:

if let label, !label.isEmpty {
    print(label)
}

A missing value and an empty value can mean different things. An optional type tells us whether there is a value at all. Checking for an empty string or another rule is a separate step: Swift cannot know those rules from String? alone.

Changing a local value, not the original

if let creates a constant. If we need a variable, we can use if var:

let title: String? = "Draft"

if var title {
    title += "!"
    print(title) // Draft!
}

print(title as Any) // Optional("Draft")

Inside if, we changed only the local variable title. The original optional stayed the same. Because both names look alike, this difference is easy to miss.

Poof! Let’s rewind...

In #003 — guard: Keep the Main Path Flat, we looked at guard let. After a successful guard check, the new value is available in the rest of the current scope because a failed check must exit that scope. With if let, the new value is available only inside the successful branch.

Getting values one at a time

We can also use optional binding in a loop. An iterator’s next() method returns the next element, or nil when there are no more elements:

var iterator = ["Mira", "Noah"].makeIterator()

while let name = iterator.next() {
    print(name)
}

The loop runs for each name and ends when next() returns nil. For a normal array, for is simpler. But while let is useful when every step needs to check whether another value is available.

Try it in code

The Molecule #004 companion includes successful and failed conversions, several checks, changes to local values, and an iterator. Try changing "42" to "forty-two", then compare nil with an empty string.

Optional binding does not change the original optional. It gives a new name to a value whose presence Swift has already checked. That is why it matters to understand the difference between if let, guard let, and extra checks required by the application.

Sources