【问题标题】:iOS 7, corrupt UINavigationBar when swiping back fast using the default interactivePopGestureRecognizeriOS 7,使用默认的 interactivePopGestureRecognizer 快速回扫时损坏 UINavigationBar
【发布时间】:2014-08-05 06:10:07
【问题描述】:

我遇到了一个问题,但我不知道为什么会发生;如果我将细节控制器推入堆栈,并使用默认的左边缘interactivePopGestureRecognizer 快速向后滑动,我的父/根视图控制器的UINavigationBar 看起来已损坏或其他东西,几乎就像内置的 iOS 转换机制没有在详细视图消失后,有时间完成重置它的工作。还要澄清一下,这个“损坏”UINavigationBar 中的所有内容仍然是可触摸的,并且我的父/根视图控制器上的所有内容都可以正常工作。

对于因没有源代码而投反对票的人:没有源代码!这是 Apple 的错误!

有没有办法将此UINavigationBar 重置为调用父/根视图控制器的 viewDidAppear 方法时的状态?

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

编辑:我添加了一个 NSLog 来检查父/根视图控制器上 viewDidAppear 上导航栏的子视图计数,并且计数始终相同,无论是否损坏,所以我想知道为什么弹出的控制器正在运行对我的UINavigationBar 造成严重破坏。

如果您能帮到我,我将不胜感激!谢谢。

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

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

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

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

【问题讨论】:

  • 这是否发生在模拟器和/或实际设备中?如果是设备,您使用的是哪种设备?
  • 发生在模拟器和任何 iOS 设备上,以及任何高于 7.0 的 iOS 版本上
  • 这很奇怪,你能发布任何代码或示例项目来说明这一点,因为我似乎根本无法复制这种行为。
  • 是的,刚刚测试了您的代码并查看了您所描述的问题。我尝试通过 segue 推送第二个 vc 以查看是否有帮助,但您仍然可以通过该方法解决问题。代码很简单,它可能是 UINavigationController 的错误。我不知道为什么我不能在我自己的应用程序或我下载的任何应用程序中复制它。抱歉,我无法提供更多帮助。
  • 不,也发生在设备上

标签: ios ios7 uinavigationcontroller uinavigationbar


【解决方案1】:

TL;DR

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

问题和解决方案

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

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

总结

  1. 监听过渡的交互部分何时结束
  2. 收集导航栏中所有视图层的动画
  3. 复制并稍微修改这些动画(将 fromValue 设置为与 toValue 相同的内容,将持续时间设置为零,以及其他一些内容)
  4. 监听过渡的非交互部分何时结束
  5. 将复制/修改的动画应用回视图层

代码

这是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];
}

【讨论】:

  • 很好的解决方案。我认为这行得通。但是您为什么不尝试使用不同的动画自定义 UINavigationControllerDelegate 呢?在过去的 2 天里,我一直在尝试这样做,但没有产生好的结果。
  • @Ricky 谢谢 Ricky,但我不完全确定我理解你的问题。
  • 您可以查看github.com/objcio/issue5-view-controller-transitions上的示例项目我相信这个问题的问题与动画有关。这个问题可以通过改变fromViewController.view.transform来解决。我不完全确定,因为我在学习不同视图控制器之间的转换方面还是新手。
  • @Ricky 但是@troop231 没有进行任何类型的自定义转换。它只是一个标准的导航控制器弹出,使用 iOS 7 中引入的interactivePopGestureRecognizer
  • 我明白这一点。我相信通过稍微更改代码来模仿标准弹出过渡的自定义过渡可能会解决问题。我可能是错的,因为我在这方面没有太多的知识。我还在学习。
【解决方案2】:

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

1) 在模拟器上,如果使用配置文件/自动化模板并添加以下脚本,则每次都可以重新创建错误:

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% 确定这也会发生在设备上吗?

【讨论】:

  • 是的,我确定.. 否则我不会发布问题
  • 在这种情况下,该错误似乎不在 UIKit 级别,而是在更深层次 - 可能是核心动画或核心图形。
  • 有没有办法找到 CALayers 并修复它们?
  • 我正在检查,但到目前为止唯一修复的是后退按钮(它隐藏)。如果您强制重绘图层甚至更改字体大小,标题将保持原样。
  • 关于数字2,我也可以确认这个问题出现在真机,iPhone 5S,iOS 7.0.6上。
【解决方案3】:

关键概念

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

一种常见的解决方案:子类化

您可以继承 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 方法来完成。想了解method swizzling,请访问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 自动执行,无需任何代码。

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

pod 'SwipeBack'

安装 pod,就完成了!

【讨论】:

  • @EmrahAkgül,你能在 GitHub 上用你的测试环境提出问题吗? (iOS版、SwipeBack版等)
猜你喜欢
  • 1970-01-01
  • 2016-08-27
  • 1970-01-01
  • 1970-01-01
  • 2013-02-15
  • 1970-01-01
  • 2019-08-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多