I have an issue that I'm stuck on, but I have no idea why it even happens; If I push a detail controller on the stack, and I swipe back very quickly using the default left edge interactivePopGestureRecognizer
, my parent/root view controller's UINavigationBar
looks corrupt or something, almost like the built in iOS transition mechanism didn't have time to do it's job at resetting it after the detail view is gone. Also to clarify, everything in this 'corrupt' UINavigationBar
is still touchable and everything on my parent/root view controller works perfectly.
For people downvoting due to no source code: there is no source code! This is an Apple bug!
Is there anyway to reset this UINavigationBar
to what it should be when the parent/root view controller's viewDidAppear method gets called?
Note that this bug does not occur if I tap the top left back button instead of using the left edge interactivePopGestureRecognizer
.
Edit: I added an NSLog to check the navigationBar's subview count on viewDidAppear on the parent/root view controller, and the count is always the same, corrupt or not, so I'd like to know why the popped controller is wreaking havoc with my UINavigationBar
.
If you can help me at all, I'd greatly appreciate it! Thank you.
I've attached a screenshot of what it looks like: Note that the back chevron isn't part of my parent/root view controller, it's part of what was popped off the stack. Testing123 is the title for the parent/root view controller and not that of what was popped off the stack. The head and gear icons are part of the parent/root view controller.
Edit: I've thought something like this could fix the issue, but turns out it doesn't, and is really bad experience IMO too. This is not the kind of solution I'm looking for. I'm posting a large bounty so this can be resolved correctly! 😃. I just can't have this weird UI behavior be in a production quality app.
- (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self.navigationController pushViewController:[UIViewController new] animated:NO]; [self.navigationController popToRootViewControllerAnimated:YES]; }
I made a category on UIViewController
that hopefully fixes this issue for you. I can't actually reproduce the navigation bar corruption on a device, but I can do it on the simulator pretty frequently, and this category solves the problem for me. Hopefully it also solves it for you on the device.
I actually don't know exactly what causes this, but the navigation bar's subviews' layers' animations seem to either be executing twice or not fully completing or... something. Anyway, I found that you can simply add some animations to these subviews in order to force them back to where they should be (with the right opacity, color, etc). The trick is to use your view controller's transitionCoordinator
object and hook into a couple of events – namely the event that happens when you lift your finger up and the interactive pop gesture recognizer finishes and the rest of the animation starts, and then the event that occurs when the non-interactive half of the animation finishes.
You can hook into these events using a couple methods on the transitionCoordinator
, specifically notifyWhenInteractionEndsUsingBlock:
and animateAlongsideTransition:completion:
. In the former, we create copies of all of the current animations of the navbar's subviews' layers, modify them slightly, and save them so we can apply them later when the non-interactive portion of the animation finishes, which is in the completion block of the latter of those two methods.
And here's the code for the UIViewController
category:
@interface UIViewController (FixNavigationBarCorruption) - (void)fixNavigationBarCorruption; @end @implementation UIViewController (FixNavigationBarCorruption) /** * Fixes a problem where the navigation bar sometimes becomes corrupt * when transitioning using an interactive transition. * * Call this method in your view controller's viewWillAppear: method */ - (void)fixNavigationBarCorruption { // Get our transition coordinator id<UIViewControllerTransitionCoordinator> coordinator = self.transitionCoordinator; // If we have a transition coordinator and it was initially interactive when it started, // we can attempt to fix the issue with the nav bar corruption. if ([coordinator initiallyInteractive]) { // Use a map table so we can map from each view to its animations NSMapTable *mapTable = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory capacity:0]; // This gets run when your finger lifts up while dragging with the interactivePopGestureRecognizer [coordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) { // Loop through our nav controller's nav bar's subviews for (UIView *view in self.navigationController.navigationBar.subviews) { NSArray *animationKeys = view.layer.animationKeys; NSMutableArray *anims = [NSMutableArray array]; // Gather this view's animations for (NSString *animationKey in animationKeys) { CABasicAnimation *anim = (id)[view.layer animationForKey:animationKey]; // In case any other kind of animation somehow gets added to this view, don't bother with it if ([anim isKindOfClass:[CABasicAnimation class]]) { // Make a pseudo-hard copy of each animation. // We have to make a copy because we cannot modify an existing animation. CABasicAnimation *animCopy = [CABasicAnimation animationWithKeyPath:anim.keyPath]; // CABasicAnimation properties // Make sure fromValue and toValue are the same, and that they are equal to the layer's final resting value animCopy.fromValue = [view.layer valueForKeyPath:anim.keyPath]; animCopy.toValue = [view.layer valueForKeyPath:anim.keyPath]; animCopy.byValue = anim.byValue; // CAPropertyAnimation properties animCopy.additive = anim.additive; animCopy.cumulative = anim.cumulative; animCopy.valueFunction = anim.valueFunction; // CAAnimation properties animCopy.timingFunction = anim.timingFunction; animCopy.delegate = anim.delegate; animCopy.removedOnCompletion = anim.removedOnCompletion; // CAMediaTiming properties animCopy.speed = anim.speed; animCopy.repeatCount = anim.repeatCount; animCopy.repeatDuration = anim.repeatDuration; animCopy.autoreverses = anim.autoreverses; animCopy.fillMode = anim.fillMode; // We want our new animations to be instantaneous, so set the duration to zero. // Also set both the begin time and time offset to 0. animCopy.duration = 0; animCopy.beginTime = 0; animCopy.timeOffset = 0; [anims addObject:animCopy]; } } // Associate the gathered animations with each respective view [mapTable setObject:anims forKey:view]; } }]; // The completion block here gets run after the view controller transition animation completes (or fails) [coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> context) { // Iterate over the mapTable's keys (views) for (UIView *view in mapTable.keyEnumerator) { // Get the modified animations for this view that we made when the interactive portion of the transition finished NSArray *anims = [mapTable objectForKey:view]; // ... and add them back to the view's layer for (CABasicAnimation *anim in anims) { [view.layer addAnimation:anim forKey:anim.keyPath]; } } }]; } } @end
And then just call this method in your view controller's viewWillAppear:
method (in your test project's case, it would be the ViewController
class):
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self fixNavigationBarCorruption]; }
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