Handling non-optional optionals in Swift

Optionals are arguably one of the most important features of Swift, and key to what sets it apart from languages like Objective-C. By being forced to handle the case where something could be nil, we tend to write more predictable and less error prone code.

However, sometimes optionals can put you in somewhat of a tight spot, where you as the programmer know (or at least, you’re under the assumption) that a certain variable will always be non-nil when used, even if it’s of an optional type. Like when handling views in a view controller:

class TableViewController: UIViewController {
    var tableView: UITableView?

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView = UITableView(frame: view.bounds)
        view.addSubview(tableView!)
    }

    func viewModelDidUpdate(_ viewModel: ViewModel) {
        tableView?.reloadData()
    }
}

This is where Swift programmers will disagree to almost the same level as with tabs vs spaces. Some say:

“Since it’s an optional, you should always properly unwrap it, using either if let or guard let.”

While others will go in the completely different direction and say:

“Since you know the variable will not be nil, force unwrap it (using !). Crashing is better than ending up in an undefined state.”

Basically what we’re talking about here, is whether or not to do defensive programming. Do we try to recover from an undefined state, or do we simply give up and crash?

If I had to give a binary answer to that question — I’d most definitely go with the latter. Undefined states lead to really hard to track down bugs, may lead to unwanted code execution and employing defensive programming will just lead to code that is hard to reason about.

But, I’d prefer not to have to give a binary answer, and instead look into some techniques that we can use to work around this problem in a much more nuanced way. Let’s dive in!

Is it really optional?

Variables and properties that are optionals, but are actually required by the program logic, are actually a symptom of an architectural flaw. If something is needed, to the point where not having it puts you in an undefined state — it shouldn’t be optional.

While there are cases (such as when interacting with certain system APIs) where optionals are really hard to avoid — there are some techniques we can use to get rid of optionals in many situations.

Being lazy is better than being non-optionally optional

One way of avoiding optionals for properties which values need to be created after the parent object is created (such as views in a view controller — which should be created in loadView() or viewDidLoad()) is through the use of lazy properties. A lazy property can be non-optional, while still not being required to be created in its parent’s initializer. It will simply be created when first accessed.

Let’s update our TableViewController from before to use a lazy property for its tableView:

class TableViewController: UIViewController {
    lazy var tableView = UITableView()

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.frame = view.bounds
        view.addSubview(tableView)
    }

    func viewModelDidUpdate(_ viewModel: ViewModel) {
        tableView.reloadData()
    }
}

No optionals, no undefined state! 🎉

Proper dependency management is better than non-optional optionals

Another common use of optionals is to break circular dependencies. You can sometimes get into situations where A depends on B, but B also depends on A. Like in this setup:

class UserManager {
    private weak var commentManager: CommentManager?

    func userDidPostComment(_ comment: Comment) {
        user.totalNumberOfComments += 1
    }

    func logOutCurrentUser() {
        user.logOut()
        commentManager?.clearCache()
    }
}

class CommentManager {
    private weak var userManager: UserManager?

    func composer(_ composer: CommentComposer
                  didPostComment comment: Comment) {
        userManager?.userDidPostComment(comment)
        handle(comment)
    }

    func clearCache() {
        cache.clear()
    }
}

As we can see above, we have a circular dependency between UserManager and CommentManager, where neither of them assumes ownership over the other, but they still rely on each other for part of their logic. That is just bugs waiting to happen! 😅

To solve the above problem, we’re instead going to make CommentComposer act as a middleman, and take on the responsibility of notifying both UserManager and CommentManager that a comment has been made:

class CommentComposer {
    private let commentManager: CommentManager
    private let userManager: UserManager
    private lazy var textView = UITextView()

    init(commentManager: CommentManager,
         userManager: UserManager) {
        self.commentManager = commentManager
        self.userManager = userManager
    }

    func postComment() {
        let comment = Comment(text: textView.text)
        commentManager.handle(comment)
        userManager.userDidPostComment(comment)
    }
}

That way, UserManager can hold a strong reference to CommentManager, without any retain (or dependency) cycles:

class UserManager {
    private let commentManager: CommentManager

    init(commentManager: CommentManager) {
        self.commentManager = commentManager
    }

    func userDidPostComment(_ comment: Comment) {
        user.totalNumberOfComments += 1
    }
}

We have again removed all optionals and have predictable code! 🎉

Crashing gracefully

Above we saw a couple of examples where we could tweak our code to remove uncertainty by removing optionals. However, sometimes that’s not possible. Let’s say you are loading a local JSON file containing the configuration for your app. This is inherently an operation that can fail, so we’re going to need to add some error handling.

Continuing program execution in case the config fails to load will put the app in an undefined state, so in this case it’s better to crash. That way we get a crash report, and hopefully our tests & QA process will catch this problem long before it reaches our users.

So, how do we crash? The simplest solution is to simply use the ! operator, to force-unwrap the optional, causing a crash if it contains nil:

let configuration = loadConfiguration()!

While this approach is simple, it comes with a pretty big downside. If this code starts crashing, all we get as an error message is:

fatal error: unexpectedly found nil while unwrapping an Optional value

The error message doesn’t tell us why and where the error occurred, and gives us no clues on how to fix it. Instead, let’s use the guard statement, combined with the preconditionFailure() function, to exit with a custom message.

guard let configuration = loadConfiguration() else {
    preconditionFailure("Configuration couldn't be loaded. " +
                        "Verify that Config.JSON is valid.")
}

When crashing using the above, we will get a much more helpful error message:

fatal error: Configuration couldn’t be loaded. Verify that Config.JSON is valid.: file /Users/John/AmazingApp/Sources/AppDelegate.swift, line 17

We now have a clear action we can take to fix the problem, and we know exactly where in our codebase it occurs! 🚀

Introducing Require

Doing the above guard-let-preconditionFailure dance can be a bit tedious, and it does make the code a bit harder to follow. We really don’t want to give extraordinary circumstances like this so much room in our code — we want to focus on our logic.

My solution to that is Require. It adds a simple require() method on Optional that does the above, but leaves the call site a lot more clean. Here’s what the above configuration loading code looks like when using Require:

let configuration = loadConfiguration().require(hint: "Verify that Config.JSON is valid")

Which will give us the following error message if it fails:

fatal error: Required value was nil. Debugging hint: Verify that Config.JSON is valid: file /Users/John/AmazingApp/Sources/AppDelegate.swift, line 17

Another advantage of Require is that it will also throw an NSException as well as calling preconditionFailure, which will enable crash reporting tools like Crashlytics to pick up all the metadata of the crash.

Require is open source on GitHub, if you want to use it in your code.

Summary

So to summarize, these are my tips for handling non-optional optionals in Swift:

  1. Being lazy is better than being non-optionally optional

  2. Proper dependency management is better than non-optional optionals

  3. Crash gracefully when you need to use non-optional optionals

Feel free to reach out to me on Twitter if you have any questions, suggestions or feedback. I’d also love to hear from you if you have any topic that you’d like me to cover in an upcoming post.

Thanks for reading 🚀

Building a command line tool using the Swift Package Manager

Namespacing Swift code with nested types