Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Optional Chaining vs Optional Binding: When to use which?

While searching for a propery way to refresh the UINavigationBar (see: How to properly refresh a UINavigationBar?) I came across this question:

This refreshes the UINavigationBar as intended, even though not in a clean way:

if let navigationController = self.navigationController {
    navigationController.popViewControllerAnimated(false)
    navigationController.pushViewController(self, animated: false)
}

However, this pops the current view controller, but does not push it:

navigationController?.popViewControllerAnimated(false)
navigationController?.pushViewController(self, animated: false)

How is this behaviour explainable?

like image 770
MJQZ1347 Avatar asked Feb 06 '23 07:02

MJQZ1347


2 Answers

Optional binding stores the thing you're binding in a variable. In this case, it's navigationController.

On the other hand, optional chaining does not put the value on the left into a variable. it only says

I will check whether this value on the left of the question mark is nil. If it isn't, evaluate the rest of the expression. If it is, evaluate to nil

So after you popped the view controller (first line), self.navigationController becomes nil and the method call in the second line is hence not evaluated.

You might have these confusions:

Why does self.navigationController become nil?

One possibility is that self is the top view controller of navigationController. After self is popped, it doesn't belong to any navigation controllers.

Why does optional binding work but not optional chaining?

As I said before, optional binding puts the value of self.navigationController into a variable navigationController. Thus, even though self.navigationController is nil, navigationController maintains its value.

like image 174
Sweeper Avatar answered Feb 13 '23 05:02

Sweeper


Optional binding allows an entire block of logic to happen the same way every time. Multiple lines of optional chaining can make it unclear exactly what is going to happen and could potentially be subject to race-conditions causing unexpected behavior.

With optional binding, you are creating a new reference to whatever you just unwrapped, and generally, that's a strong reference. It's also a local reference that is not going to be subject to mutation from other cases. It's also generally let by habit. So it will retain the same value throughout its lifetime.

Consider your example... but with some slight tweaks.

if let navigationController = self.navigationController {

    // this should always pass
    assert(navigationController === self.navigationController)

    navigationController.popViewControllerAnimated(false)

    // this next assert may fail if `self` refers to the top VC on the nav stack
    assert(navigationController === self.navigationController)

    // we now add self back onto the nav stack
    navigationController.pushViewController(self, animated: false)

    // this should also always pass:
    assert(navigationController === self.navigationController)
}

This is happening because as a navigation controller adds a new view controller to its navigation stack (with pushViewController), it sets the passed in view controller's navigationController property equal to itself. And when that view controller is popped off the stack, it sets that property back to nil.

Let's take a look at the optional-chaining approach, throwing these assertions in again:

let navController = navigationController

// this should always pass
assert(navController === navigationController)

// assuming navigationController does not evaluate to nil here
navigationController?.popViewControllerAnimated(false)

// this may fail if `self` is top VC on nav stack
assert(navController === navigationController)

// if above failed, it's because navigation controller will now evaluate to nil
// if so, then the following optional chain will fail and nothing will happen
navigationController?.pushViewController(self, animated: false)

// if the previous assert fail, then the previous push wasn't called
// and if the previous push wasn't called, then navigation controller is still nil
// and therefore this assert would also fail:
assert(navController === navigationController)

So here, our instance property on self is being set to nil by our own method calls causing this difference in behavior between optional binding & chaining.

But even if we weren't mutating that property by our own actions on the current thread, we can still run into the issue where suddenly the property is evaluating to nil even though it had a valid value on the previous line. We can run into this with multithreaded code, where another thread might cause our property which had a value to now evaluate to nil or vice-versa.

In short, if you need to make sure of all-or-none, you need to prefer optional-binding.

like image 36
nhgrif Avatar answered Feb 13 '23 07:02

nhgrif