We started getting this issue as well, and chances were highly likely that ours were caused by the same problem.
In our case, we had to pull data from the back end in some cases, which meant a user might tap something and then there'd be a slight delay before the nav push occurred. If a user was rapidly tapping around, they might end up with two nav pushes from the same view controller, which triggered this very exception.
Our solution is a category on the UINavigationController which prevents pushes/pops unless the top vc is the same one from a given point in time.
.h file:
@interface UINavigationController (SafePushing)
- (id)navigationLock; ///< Obtain "lock" for pushing onto the navigation controller
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated navigationLock:(id)navigationLock; ///< Uses a horizontal slide transition. Has no effect if the view controller is already in the stack. Has no effect if navigationLock is not the current lock.
- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated navigationLock:(id)navigationLock; ///< Pops view controllers until the one specified is on top. Returns the popped controllers. Has no effect if navigationLock is not the current lock.
- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated navigationLock:(id)navigationLock; ///< Pops until there's only a single view controller left on the stack. Returns the popped controllers. Has no effect if navigationLock is not the current lock.
@end
.m file:
@implementation UINavigationController (SafePushing)
- (id)navigationLock
{
return self.topViewController;
}
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated navigationLock:(id)navigationLock
{
if (!navigationLock || self.topViewController == navigationLock)
[self pushViewController:viewController animated:animated];
}
- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated navigationLock:(id)navigationLock
{
if (!navigationLock || self.topViewController == navigationLock)
return [self popToRootViewControllerAnimated:animated];
return @[];
}
- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated navigationLock:(id)navigationLock
{
if (!navigationLock || self.topViewController == navigationLock)
return [self popToViewController:viewController animated:animated];
return @[];
}
@end
So far this seems to have resolved the problem for us. Example:
id lock = _dataViewController.navigationController.navigationLock;
[[MyApi sharedClient] getUserProfile:_user.id success:^(MyUser *user) {
ProfileViewController *pvc = [[ProfileViewController alloc] initWithUser:user];
[_dataViewController.navigationController pushViewController:pvc animated:YES navigationLock:lock];
}];
Basically, the rule is: before any non user related delays grab a lock from the relevant nav controller, and include it in the call to push/pop.
The word "lock" may be slightly poor wording as it may insinuate there's some form of lock happening that needs unlocking, but since there's no "unlock" method anywhere, it's probably okay.
(As a sidenote, "non user related delays" are any delays that the code is causing, i.e. anything asynchronous. Users tapping on a nav controller which is animatedly pushed doesn't count and there's no need to do the navigationLock: version for those cases.)