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.