Weekly Swift articles, podcasts and tips by John Sundell.

Optionals

Published on 11 Feb 2020

One key part of Swift’s overall design is how it requires us to explicitly handle values that might be missing, or optional. While that requirement often forces us to think more thoroughly about how we construct our objects and manage our state — it also arguably leads to fewer unhandled runtime errors caused by missing data.

In Swift, a value is marked as optional by adding a question mark right after its type, which in turn requires us to unwrap that value before it can be used in any concrete way. For example, here we’re using an if let statement to unwrap an optional User value in order to determine whether a user has logged into our app:

func setupApp(forUser user: User?) {
    if let user = user {
        showHomeScreen(for: user)
    } else {
        showLoginScreen()
    }
}

While there are multiple ways to unwrap and handle optionals, another very common pattern is to use the guard statement to return early in case an optional value is missing. Here’s how we might refactor the above function to use that pattern instead:

func setupApp(forUser user: User?) {
    guard let user = user else {
        // Guard statements require us to "exit" out of the
        // current scope, for example by returning:
        return showLoginScreen()
    }

    showHomeScreen(for: user)
}

Essentially, optionals provide us with a built-in way to represent the “lack of a value” — which make them an ideal choice in situations when we need to model some form of default or missing state.

For example, here we’ve created a Relationship enum, which enables us to express the relationship between two users of our app. Currently, we represent the lack of any relationship using a none case within that enum, which also acts as our default value:

enum Relationship {
    case none
    case friend
    case family
    case coworker
}

struct User {
    var name: String
    var relationship: Relationship = .none
    ...
}

However, while the above code technically works, it would arguably be better to implement our relationship property as an optional instead. That way, we can use Swift’s built-in way to represent the lack of any relationship, which will both let us use mechanisms like if let and guard — and also lets us simplify our enum by removing its none case:

enum Relationship {
    case friend
    case family
    case coworker
}

struct User {
    var name: String
    var relationship: Relationship? = nil
    ...
}

However, optionals can also become a source of ambiguity if we’re not careful. It’s always important to consider whether a given value is actually optional before implementing it as such — since if a value is required in order for our code to function, we should ideally be able to guarantee that it’ll always be there.

For example, let’s have a look at a view controller implementation which currently uses optionals to store its subviews — a headerView and a logOutButton — in order to lazily create them once the system calls viewDidLoad(), which is the recommended way of constructing view controller-based UIs:

class ProfileViewController: UIViewController {
    private var headerView: HeaderView?
    private var logOutButton: UIButton?

    override func viewDidLoad() {
        super.viewDidLoad()

        let headerView = HeaderView()
        view.addSubview(headerView)
        self.headerView = headerView

        let logOutButton = UIButton()
        view.addSubview(logOutButton)
        self.logOutButton = logOutButton
        
        // More view configuration
        ...
    }
}

The above is a very common pattern, but it does come with a quite substantial problem, in that we always have to keep unwrapping our subviews as optionals — even though they are required parts of our view controller’s logic. For example, here we have to unwrap our headerView property in order to be able to assign various model values to its properties:

extension ProfileViewController {
    func userDidUpdate(_ user: User) {
        guard let headerView = headerView else {
            // This should never happen, but we still have
            // to maintain this code path.
            return
        }

        headerView.imageView.image = user.image
        headerView.label.text = user.name
    }
}

What we’re essentially dealing with above, is non-optional optionals — values that might technically be optionals, but are in fact not optional when it comes to how we’ve implemented our logic.

While removing non-optional optionals can sometimes be quite difficult, in the above case, there’s a quite straightforward way. Using Swift’s lazy keyword, we can delay the initialization of our view controller’s subviews until those properties are first accessed — giving us the exact same behavior as we had before, but without any optionals — leading to much simpler code:

class ProfileViewController: UIViewController {
    private lazy var headerView = HeaderView()
    private lazy var logOutButton = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.addSubview(headerView)
        view.addSubview(logOutButton)
        
        // More view configuration
        ...
    }

    func userDidUpdate(_ user: User) {
        headerView.imageView.image = user.image
        headerView.label.text = user.name
    }
}

For more information about using lazy properties in Swift, check out this article.

An alternative way of dealing with non-optional optionals is to use force unwrapping (using !) to turn an optional value into a concrete one, without any checks. However, while force unwrapping might occasionally be warranted, it does always come with the risk of causing a crash in case its value ended up being missing — so if we can find another solution, that’s most often preferable.

Next, let’s take a quick look under the hood of how optionals are actually implemented. The cool thing about Swift’s version of optionals is that they’re actually modeled using a standard enum, which looks like this:

enum Optional<Wrapped>: ExpressibleByNilLiteral {
    case none
    case some(Wrapped)
}

The above is just one of many examples of how Swift uses its own type system to implement many of its core language features — which is not only a really interesting design, but in this case also enables us to treat optional values just like any other enum. For example, we can switch on them:

func icon(forRelationship relationship: Relationship?) -> Icon? {
    // Here we switch on the optional itself, rather than on
    // its underlying Relationship value:
    switch relationship {
    case .some(let relationship):
        // Then, we switch on the wrapped value itself:
        switch relationship {
        case .friend:
            return .user
        case .family:
            return .familyMember
        case .coworker:
            return .work
        }
    case .none:
        return nil
    }
}

The above might be interesting in terms of how Swift’s various features work together — but the resulting code is arguably a bit hard to read, due to the many nested statements. Thankfully, Swift also enables us to switch on any optional directly — just as if we were switching on its wrapped value. All we have to do is to include a nil case to handle the lack of a value:

func icon(forRelationship relationship: Relationship?) -> Icon? {
    switch relationship {
    case .friend:
        return .user
    case .family:
        return .familyMember
    case .coworker:
        return .work
    case nil:
        return nil
    }
}

The above syntax works in Swift 5.1 and above. When using earlier versions, we have to append a question mark to each non-nil case — like this: case .friend?:.

The fact that optionals are implemented using their own, stand-alone type, also means that they can have methods and properties of their own. For example, here we’re converting a String that represents a URL into a URLRequest instance — which requires us to first optionally convert that string into a URL value, which we then pass into our new URLRequest:

func makeRequest(forURLString string: String) -> URLRequest? {
    guard let url = URL(string: string) else {
        return nil
    }

    return URLRequest(url: url)
}

The above code works, but if we wanted to, we could make it a lot more compact — by instead calling map directly on our optional URL. Similar to how we can use map to transform a collection, calling map on an optional lets us use a closure to transform any value that it’s wrapping — like this:

func makeRequest(forURLString string: String) -> URLRequest? {
    URL(string: string).map { URLRequest(url: $0) }
}

What’s really cool is that not only can we use methods and properties that come built into the Optional type, we can also define our own. For example, here’s how we could define two properties that enable us to check whether any collection is nil or empty:

extension Optional where Wrapped: Collection {
    var isNilOrEmpty: Bool {
        // If the left-hand expression is nil, the right one
        // will be used, meaning that 'true' is our default:
        self?.isEmpty ?? true
    }

    var nonEmpty: Wrapped? {
        // Either return this collection, or nil if it's empty:
        isNilOrEmpty ? nil : self
    }
}

With the above in place, we can now easily handle any missing values, and empty ones — all using a single guard statement:

extension EditorViewController: UITextFieldDelegate {
    func textFieldDidEndEditing(_ textField: UITextField) {
        guard let text = textField.text.nonEmpty else {
            return
        }

        // Handle non-empty text
        ...
    }
}

To learn more about the above technique, check out “Extending optionals in Swift”.

How we handle our optional, potentially missing values is arguably just as important as how we handle our concrete ones — and by fully utilizing Swift’s Optional type and its various features, we can often end up with code that doesn’t only have a higher chance of being correct, but that’s also very concise and elegant as well.

Thanks for reading! 🚀

Support Swift by Sundell by checking out this sponsor:

Instabug
Instabug

Instabug: Investigate, diagnose and resolve issues up to four times faster. Whether it’s a crash, slow screen transitions, slow network calls or unresponsive UIs, Instabug lets you utilize powerful performance patterns to trace the cause of each issue. Detect if a specific app version, device or network connection is affecting the user experience and spot trends and spikes. Get started now and ship apps that your users will love.