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?
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With