Skip to main content
  1. SwiftUI in 100 Days Notes/

Day 14 - Swift Optionals and Nil Coalescing

In this section, we will examine Swift’s solutions for null references (variables with no value), known as optionals. In essence, an optional tries to answer the question “what happens if our variable has no value?”.

How to Handle Missing Data with Options #

Optionals, one of Swift’s solutions to null reference, means “this thing may or may not have a value”.

Consider this code;

let opposites = [
    "Mario": "Wario",
    "Luigi": "Waluigi"
]

let peachOpposite = opposites["Peach"]

We create a [String : String] dictionary with two key values. Then we tried to read the value attached to the key “Peach” which is not in the opposites dictionary. What will be the value of peachOpposite when this code runs?

Swift’s solution to this problem is called optionals, which means data that may or may not be available. Data that will be optionals is represented by a ? after the data type. So in this case, peachOpposite will be a String? instead of a String.

Optionals are like a box that may or may not have something in it. So in a String? box we can have a string waiting for us or nothing (a special value called nil, which means “no value”). Any kind of data can be optional; Int, Double, Bool, enum instance, struct instance or class instance.

So what has changed here? Before we had String, now we have String?, where is the difference?

The difference is this: Swift wants our code to be predictable, which means it won’t let us use data that isn’t there. In the case of optionals, this means that to use optionals we need to unwrap it.

Swift offers us two basic ways to open optionals, but the one we will see the most looks like this;

if let marioOpposite = opposites["Mario"] {
    print("Mario's opposite is \(marioOpposite)")
}

This if let syntax is very common in Swift and combines the creation of a condition (if) and a constant (let). Together they do three things;

  1. Reads optional from the dictionary.
  2. If optional contains a string, this string is unwrapped. *The *unwrapped data is placed in the marioOpposite constant.
  3. If the condition succeeds, i.e. optional is unwrapped, the body of the condition is called.

The body of the condition will only be executed if there is a value in optional. Of course we can also add an else block if we want. This is just an if condition.

var username: String? = nil

if let unwrappedName = username {
    print("We got a user: \(unwrappedName)")
} else {
    print("The optional was empty.")
}

We can think of Optionals a bit like Schrödinger’s data type. There may or may not be a value inside the box, but the only way to find out is to check.

Optionals mean that data may or may not be present, while non-optionals, String, integer, etc. data must be present.

Let’s think of it this way; if we have a non-optional Int, it definitely has a number in it. In contrast, an optional Int set to nil has no value, 0 or any other number not is nothing.

Any data type can be optional if needed, including collections such as Array and Dictionary. Again, an Array of Int numbers may contain one or more numbers, or perhaps no numbers at all, but both of these are different from an optional Array set to nil.

To be clear, an optional Int set to nil is not the same as a non-optional Int holding the value 0. By nil we are talking about the absence of any data.

We can see this problem when we want to pass an optional Int as a parameter to a function that expects a non-optional Int.

func square(number: Int) -> Int {
    number * number
}

var number: Int? = nil
print(square(number: number))

swift optional vs non optional

Swift will refuse to generate this code, because an optional Int must be unwrapped.

Therefore, we need to unwrap optional.

if let unwrappedNumber = number {
    print(square(number: unwrappedNumber))
}

When doing optional unwraps, it is very common to unwrap them to a constant (let) with the same name. Swift allows this.

Using this approach, let’s rewrite our code;

if let number = number {
    print(square(number: number))
}

What happens here is that we temporarily create a second constant with the same name, which can only be used in the body of the condition. This is called shadowing and is often used in the unwrap operation.

We can also write the above code block in a shorter form;

if let number {
    print(square(number: number))
}

if let number and if let number = number do the same thing, only it creates a copy of shadow which is unwrapped inside the body of the condition.

What is Swift guard? How to use it? #

We have already seen how to use if let to unwrap the optional. There is a second way that does almost the same thing: guard let

func printSquare(of number: Int?) {
    guard let number = number else {
        print("Missing input")
        return
    }

    print("\(number) x \(number) is \(number * number)")
}

Like if let, guard let checks if there is a value in optional and if there is, it takes the value and places it in the constant of our choice.

But he does things a bit backwards.

var myVar: Int? = 3

if let unwrapped = myVar {
    print("myVar içinde değer varsa çalışır")
}

guard let unwrapped = myVar else {
    print("myVar içinde değer yoksa çalışır")
}

if let executes the code in parentheses if optional has a value. guard let executes the code in parentheses if the optional element does not have a value. This explains the use of else in our code: “check if we can unwarp the optional element, but if we can’t, then …..”

guard let is designed to exit the current function, loop or condition if the check fails, so any values we unwrap using guard let will remain after the check.

This is sometimes called early return. At the start of the function we check that all the inputs to the function are valid, and if they are not, we run some code and exit immediately. If all checks pass, our function works exactly as intended.

guard is designed for exactly this kind of programming and actually does two things;

  1. If we use guard to check if the inputs to a function are valid, Swift will always ask us to use return if the check fails.
  2. If the check passes and the optional element we unwarped has a value in it, we can use it after the guard code is finished.

If we examine the printSquare function we just wrote, we can see these two points.

func printSquare(of number: Int?) {
    guard let number = number else {
        print("Missing input")

        // 1: We *must* exit the function here
        return
    }

    // 2: `number` can still be used outside `guard`
    print("\(number) x \(number) is \(number * number)")
}

In short, use if let to unwrap the optional so that we can perform operations with these optioanal items. Use guard let to make sure that there is something inside the optional, but to exit if there is no value inside.

We can use guard with any condition, including optional unwraps. guard someArray.isEmpty else {return}

Unwrap Optionals Using Nil Coalescing #

There is another method called nil coalescing operator which allows us to unwrap an optional and assign a default value if the optional is empty.

let captains = [
    "Enterprise": "Picard",
    "Voyager": "Janeway",
    "Defiant": "Sisko"
]

let new = captains["Serenity"]

The code above reads a key that does not exist in the captains dictionary. This means that the new constant will be an optional String set to nil.

With the nil coalescing operator written as ?? we can provide a default value for any optional element as follows.

let new = captains["Serenity"] ?? "N/A"

The code above will read the value from the captains dictionary and try to open it. If there is a value in optional it will be sent back and stored in new, but if there is no value “N/A” will be used instead.

In this case, whatever optional contains (a value or nil) means that new will be a real string, not optional. So the value of new will either be a string in captains or “N/A”.

Of course, we can also provide a default value when reading values from the dictionary. So what is the advantage of nil coalescing?

let new = captains["Serenity", default: "N/A"]

The code above produces exactly the same result. However, the nil coalescing operator can work with any optional, not just dictionary.

For example, the randomElement() method on Array returns a random element from the Array, but returns optional in case we call this method on an empty Array. Therefore, we can use nil coalescing operator to provide a default.

let tvShows = ["Archer", "Babylon 5", "Ted Lasso"]
let favorite = tvShows.randomElement() ?? "None"

Or we have a Struct with an optional property and we want to provide a sensible default value for cases where it has no value;

struct Book {
    let title: String
    let author: String?
}

let book = Book(title: "Beowulf", author: nil)
let author = book.author ?? "Anonymous"
print(author)

It is useful even when we create an Int from a string, where we actually get back an optional Int? because the conversion may have failed. We may have provided a string like “Hello” which cannot be converted to Int.

let input = ""
let number = Int(input) ?? 0
print(number)

How to Process Multiple Optional Using Optional Chaining #

Optional chaining is a simplified syntax for reading the optional inside the optional.

let names = ["Arya", "Bran", "Robb", "Sansa"]

let chosen = names.randomElement()?.uppercased() ?? "No one"
print("Next in line: \(chosen)")

Optional chaining allows us to say “optional if there is a value in it, open it and then ….” and we can add more code. In our code we say “if we managed to get a random element from Array, then capitalize it”. Note that randomElement() returns an optional.

The advantage of optional chaining is that it does nothing if the optional is empty. It will just send back the same optional we had before, still empty. This means that the return value of optional chaining is always optional, so we still need nil coalescing operator to provide a default value.

Optional chaining can be as long as we want and when any part returns nil, the rest of the code line is ignored and returns nil.

For an example of optional chaining, let’s imagine that we want to place books in alphabetical order by author name. In this case;

  • We have an optional instance of a Book struct (we may or may not have a book to sort)
  • The book can have an author or be anonymous.
  • If the Author string is present, it can be an empty string or contain text. Therefore we cannot always trust that the first letter is there.
  • If there is a first letter, let’s make sure it is uppercase, so that authors with lowercase names like bell hooks are sorted correctly.
struct Book {
    let title: String
    let author: String?
}

var book: Book? = nil
let author = book?.author?.first?.uppercased() ?? "A"
print(author)

So we are saying “if we have a book, if the book has an author and the author has a first letter, then capitalize it and send it back, otherwise send back “A””.

Optionals and Function Error #

When we call a function that may throw an error, we either call it using try and handle errors appropriately, or if we are sure that the function will not fail, we risk our code crashing by using `try!

However, there is an alternative. If all we care about is whether the function succeeds or fails, we can use an optional try to make the function return an optional value. If the function ran without throwing any errors, it will contain the optional return value, but if it threw any errors, it will return nil. This means that we won’t know exactly what error was thrown, but we can only care if the function worked or not.

enum UserError: Error {
    case badID, networkFailed
}

func getUser(id: Int) throws -> String {
    throw UserError.networkFailed
}

if let user = try? getUser(id: 23) {
    print("User: \(user)")
}

The getUser() function will always throw a networkFailed error (best for testing) but we don’t care which error is thrown. We only care if the function call returns a user.

Here try? helps. It allows getUser() to send an optional string, which will be nil if any error is thrown. If we want to know exactly what error happened, this approach is not useful.

İstersek try? ile nil coalescing ‘i birleştirebiliriz. Bu durumda “bu fonksiyondan geri dönüş değerini almaya çalış, ancak başarısız olursa bunun yerine bu varsayılan değeri kullan” demiş oluruz. Bunun kod hali;

let user = (try? getUser(id: 23)) ?? "Anonymous"
print(user)

Where to Use the try? #

  1. With guard let to exit the current function if a call to try? returns nil.
  2. With nil coalescing to call a function but provide a default value in case of error.
  3. When calling any throw function with no return value, when we don’t really care if it succeeds or not. For example, we might be writing to a log file or sending analysis to a server.

100 Days of SwiftUI Checkpoint - 9 #


You can also read this article in Turkish.
Bu yazıyı Türkçe olarak da okuyabilirsiniz.

This article contains the notes I took for myself from the articles found at SwiftUI Day 14. Please use the link to follow the original lesson.