iOS 7, corrupt UINavigationBar when swiping back fast using the default interactivePopGestureRecognizer

前端 未结 3 1807
我寻月下人不归
我寻月下人不归 2021-01-31 02:50

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

相关标签:
3条回答
  • 2021-01-31 03:10

    Key Concept

    Disable gesture recognizer when pushing view controller, and enable it when view appeared.

    A Common Solution: Subclassing

    You can subclass UINavigationController and UIViewController to prevent corruption.

    MyNavigationController : UINavigationController

    - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
    {
        [super pushViewController:viewController animated:animated];
        self.interactivePopGestureRecognizer.enabled = NO; // disable
    }
    

    MyViewController : UIViewController

    - (void)viewDidAppear:(BOOL)animated
    {
        [super viewDidAppear:animated];
        self.navigationController.interactivePopGestureRecognizer.enabled = YES; // enable
    }
    

    Problem: Too annoying

    • Need to use MyNavigationController and MyViewController instead of UINavigationController and UIViewController.
    • Need to subclass for UITableViewController, UICollectionViewController, and more.

    A Better Solution: Method Swizzling

    It could be done by swizzling UINavigationController and UIViewController methods. Want to know about method swizzling, visit here.

    Example below uses JRSwizzle that makes method swizzling easy.

    UINavigationController

    + (void)load
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [self jr_swizzleMethod:@selector(viewDidLoad)
                        withMethod:@selector(hack_viewDidLoad)
                             error:nil];
            [self jr_swizzleMethod:@selector(pushViewController:animated:)
                        withMethod:@selector(hack_pushViewController:animated:)
                             error:nil];
        });
    }
    
    - (void)hack_viewDidLoad
    {
        [self hack_viewDidLoad];
        self.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)self;
    }
    
    - (void)hack_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
    {
        [self hack_pushViewController:viewController animated:animated];
        self.interactivePopGestureRecognizer.enabled = NO;
    }
    

    UIViewController

    + (void)load
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [self jr_swizzleMethod:@selector(viewDidAppear:)
                        withMethod:@selector(hack_viewDidAppear:)
                             error:nil];
        });
    }
    
    - (void)hack_viewDidAppear:(BOOL)animated
    {
        [self hack_viewDidAppear:animated];
        self.navigationController.interactivePopGestureRecognizer.enabled = YES;
    }
    

    Being Simple: Use Open Source

    SwipeBack

    SwipeBack does it automatically without any code.

    With CocoaPods, just add a line below into your Podfile. You don't need to write any code. CocoaPods automatically import SwipeBack globally.

    pod 'SwipeBack'

    Install pod, and it's done!

    0 讨论(0)
  • 2021-01-31 03:17

    TL;DR

    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.

    The Problem, and the Solution

    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.

    Summary

    1. Listen for when the interactive portion of the transition ends
    2. Gather up the animations for all the views' layers in the navigation bar
    3. Copy and modify these animations slightly (set fromValue to the same thing as the toValue, set duration to zero, and a few other things)
    4. Listen for when the non-interactive portion of the transition ends
    5. Apply the copied/modified animations back to the views' layers

    Code

    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];
    }
    
    0 讨论(0)
  • 2021-01-31 03:31

    After investigating this issue for some time with debug console, Instruments and Reveal, I have found out the following:

    1) On simulator the bug can be recreated every time, if using Profile/Automation Template and adding the following script:

    var target = UIATarget.localTarget();
    var appWindow = target.frontMostApp().mainWindow();
    appWindow.buttons()[0].tap();
    target.delay(1);
    target.flickFromTo({x:2, y: 100}, {x:160, y: 100});
    

    2) On real device (iPhone 5s, iOS 7.1) this script never causes the bug. I tried various options for flick coordinates and the delay.

    3) UINavigationBar consists of:

    _UINavigationBarBackground (doesn't seem to be related to the bug)
          _UIBackdropView
               _UIBackgropEffectView
               UIView
          UIImageView
    UINavigationItemView
          UILabel (visible in the bug)
    _UINavigationBarBackIndicatorView (visible in the bug)
    

    4) When bug happens UILabel looks half transparent and in the wrong position, but the actual properties of the UILabel are correct (alpha: 1 and frame as in normal situation). Also _UINavigationBarBackIndicatorView looks doesn't correspond to actual properties - it is visible although it's alpha is 0.

    From this I conclude that it's a bug of Simulator and that you can't even detect from the code that something is wrong.

    So @troop231 - are you 100% sure this also happens on device?

    0 讨论(0)
提交回复
热议问题