使用默认的interactivePopGestureRecognizer快速向后滑动时,iOS 7损坏了UINavigationBar

时间:2014-06-15 03:51:53

标签: ios ios7 uinavigationcontroller uinavigationbar

我有一个问题,我坚持,但我不知道为什么会发生这种情况;如果我在堆栈上推送一个细节控制器,并且我使用默认的左边缘interactivePopGestureRecognizer快速向后滑动,我的父/根视图控制器的UINavigationBar看起来很腐败或类似的东西,几乎就像内置的iOS转换机制在详细信息视图消失后没有时间重置它。另外要澄清的是,这一切中的一切都是“腐败的”。 UINavigationBar仍然是可触摸的,我的父/根视图控制器上的所有内容都可以正常工作。

由于没有源代码而导致人们失败:没有源代码!这是一个Apple bug!

是否有将UINavigationBar重置为父/ root视图控制器的viewDidAppear方法被调用时应该是什么?

请注意,如果我点按左上方的后退按钮而不是使用左边缘interactivePopGestureRecognizer,则不会发生此错误。

编辑:我添加了一个NSLog来检查父/根视图控制器上viewDidAppear上的navigationBar的子视图计数,并且计数总是相同的,是否已损坏,所以我想知道为什么弹出的控制器会对我的UINavigationBar造成严重破坏。

如果你可以帮助我,我会非常感激!谢谢。

我附上了它的外观截图:请注意,后面的V形图不是我的父/根视图控制器的一部分,它是从堆栈中弹出的部分。 Testing123是父/根视图控制器的标题,而不是从堆栈弹出的标题。头部和齿轮图标是父/根视图控制器的一部分。

编辑:我认为这样的事情可以解决这个问题,但事实证明它并没有,而且IMO的体验也非常糟糕。这不是我正在寻找的那种解决方案。我发布了大笔奖金,所以这可以正确解决! 。我只是不能在生产质量的应用程序中使用这种奇怪的UI行为。

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    [self.navigationController pushViewController:[UIViewController new] animated:NO];
    [self.navigationController popToRootViewControllerAnimated:YES];
}

enter image description here

3 个答案:

答案 0 :(得分:20)

TL; DR

我在UIViewController上创建了一个类别,希望能为您解决此问题。我实际上无法在设备上重现导航栏损坏,但我可以在模拟器上经常这样做,而这个类别为我解决了这个问题。希望它也可以在设备上为您解决。

问题和解决方案

我实际上并不确切知道是什么导致这种情况,但导航栏的子视图'层'动画似乎要么执行两次,要么没有完全完成或......某事。无论如何,我发现你可以简单地为这些子视图添加一些动画,以迫使它们回到它们应该的位置(具有正确的不透明度,颜色等)。诀窍是使用您的视图控制器的transitionCoordinator对象并挂钩几个事件 - 即当您抬起手指并且交互式弹出手势识别器完成时发生的事件以及动画的其余部分启动,然后是动画的非交互式一半结束时发生的事件。

您可以使用transitionCoordinator上的几种方法,特别是notifyWhenInteractionEndsUsingBlock:animateAlongsideTransition:completion:来挂钩这些事件。在前者中,我们创建了导航栏子视图的所有当前动画的副本。图层,稍微修改它们并保存它们,以便我们可以在动画的非交互部分完成时应用它们,这在后两种方法的完成块中。

摘要

  1. 在过渡的交互部分结束时收听
  2. 收集所有观看动画的动画'导航栏中的图层
  3. 稍微复制和修改这些动画(将fromValue设置为与toValue相同,将持续时间设置为零,以及其他一些事项)
  4. 在转换的非交互部分结束时收听
  5. 将复制/修改的动画应用回视图'层
  6. 代码

    这里是UIViewController类别的代码:

    @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
    

    然后在视图控制器的viewWillAppear:方法中调用此方法(在您的测试项目中,它将是ViewController类):

    - (void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];
    
        [self fixNavigationBarCorruption];
    }
    

答案 1 :(得分:3)

在使用调试控制台,仪器和Reveal调查此问题一段时间之后,我发现了以下内容:

1)在模拟器上,如果使用Profile / Automation Template并添加以下脚本,则每次都可以重新创建bug:

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)在真实设备(iPhone 5s,iOS 7.1)上,此脚本永远不会导致错误。我为轻弹坐标和延迟尝试了各种选项。

3)UINavigationBar包括:

_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)当发生错误时,UILabel看起来是半透明的并且处于错误的位置,但是UILabel的实际属性是正确的(alpha:1和正常情况下的帧)。另外_UINavigationBarBackIndicatorView看起来与实际属性不对应 - 虽然alpha为0,但它是可见的。

由此我得出结论,这是模拟器的一个错误,你甚至无法从代码中检测到出现了问题。

所以@ troop231 - 您是否100%确定这也会发生在设备上?

答案 2 :(得分:0)

关键概念

按下视图控制器时禁用手势识别器,并在视图出现时启用它。

常见解决方案:子类化

您可以继承UINavigationControllerUIViewController以防止损坏。

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
}

问题:太烦人了

  • 需要使用MyNavigationControllerMyViewController代替UINavigationControllerUIViewController
  • 需要为UITableViewControllerUICollectionViewController等创建子类。

更好的解决方案:方法调整

可以通过调整UINavigationControllerUIViewController方法来完成。想要了解方法调配,请访问here

下面的示例使用JRSwizzle,使方法变得容易。

的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;
}

简单:使用开源

SwipeBack

SwipeBack 会在没有任何代码的情况下自动执行此操作。

使用CocoaPods,只需在Podfile下方添加一行即可。您不需要编写任何代码。 CocoaPods全局自动导入SwipeBack。

pod 'SwipeBack'

安装pod,它就完成了!