IOS 7, поврежденный UINavigationBar при быстром обращении, используя встроенный по умолчанию интерактивныйPopGestureRecognizer

У меня проблема, что я застрял, но я понятия не имею, почему это происходит; Если я нажму контроллер детали в стеке, и я быстро вернусь назад, используя левый край по умолчанию interactivePopGestureRecognizer, мой родительский/корневой контроллер представления UINavigationBar выглядит коррумпированным или что-то вроде того, что встроенный механизм перехода iOS не есть время, чтобы сделать это, чтобы сбросить его после исчезновения подробного представления. Также, чтобы уточнить, все в этом "коррумпированном" UINavigationBar по-прежнему ощутимо, и все на моем контроллере родительского/корневого представления отлично работает.

Для людей, не имеющих исходного кода: нет исходного кода! Это ошибка Apple!

Есть ли все-таки reset этот UINavigationBar в зависимости от того, каким он должен быть, когда вызывается метод viewDidAppear родительского/корневого представления?

Обратите внимание, что эта ошибка не возникает, если я нажимаю верхнюю левую кнопку назад вместо левого края interactivePopGestureRecognizer.

Изменить: я добавил NSLog, чтобы проверить подсчет subview navigationBar на viewDidAppear на контроллере родительского/корневого представления, и подсчет всегда один и тот же, поврежденный или нет, поэтому я хотел бы знать, почему всплывающий контроллер рушится havoc с моим UINavigationBar.

Если вы можете мне помочь, я бы очень признателен! Спасибо.

Я прикрепил скриншот того, как он выглядит: Обратите внимание, что back chevron не является частью моего контроллера родительского/корневого представления, это часть того, что было вытолкнуто из стека. Testing123 - это заголовок для контроллера родительского/корневого представления, а не того, что было вытолкнуто из стека. Значки головки и шестерни являются частью контроллера родительского/корневого представления.

Изменить: я думал, что что-то подобное может решить проблему, но, оказывается, это не так, и это действительно плохой опыт ИМО тоже. Это не то решение, которое я ищу. Я отправляю большую щедрость, чтобы это можно было решить правильно! 😃. У меня просто не может быть такого странного поведения пользовательского интерфейса в приложении для качественного производства.

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

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

enter image description here

Ответ 1

TL; DR

Я сделал категорию на UIViewController, которая, надеюсь, исправляет эту проблему для вас. Я не могу воспроизвести повреждение навигационной панели на устройстве, но я могу сделать это на симуляторе довольно часто, и эта категория решает проблему для меня. Надеюсь, он также решит его для вас на устройстве.

Проблема и решение

На самом деле я точно не знаю, что именно вызывает это, но анимация слоев на панели навигации кажется, что либо выполняется дважды, либо не полностью завершает или... что-то. Во всяком случае, я обнаружил, что вы можете просто добавить некоторые анимации в эти подпрограммы, чтобы вернуть их туда, где они должны быть (с правильной непрозрачностью, цветом и т.д.). Хитрость состоит в том, чтобы использовать ваш объект управления transitionCoordinator и подключаться к нескольким событиям, а именно к событию, которое происходит, когда вы поднимаете палец вверх, а распознаватель интерактивного распознавания поцелуя заканчивается, а остальная часть анимации начинается, а затем событие это происходит, когда неинтерактивная половина анимации заканчивается.

Вы можете подключиться к этим событиям, используя пару методов на transitionCoordinator, в частности notifyWhenInteractionEndsUsingBlock: и animateAlongsideTransition:completion:. В первом мы создаем копии всех текущих анимаций слоев subviews navbar, немного модифицируем их и сохраняем их, чтобы мы могли применять их позже, когда неинтерактивная часть анимации заканчивается, которая находится в блоке завершения последнего из этих двух методов.

Резюме

  • Слушайте, когда заканчивается интерактивная часть перехода.
  • Собирайте анимации для всех слоев вида в панели навигации
  • Скопируйте и измените эти анимации немного (установите fromValue на то же, что и forValue, установите длительность на ноль и несколько других вещей)
  • Слушайте, когда неинтерактивная часть перехода заканчивается.
  • Примените скопированные/модифицированные анимации к слоям просмотров

код

И вот код для категории 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 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 nav bar subviews
            for (UIView *view in self.navigationController.navigationBar.subviews) {

                NSArray *animationKeys = view.layer.animationKeys;
                NSMutableArray *anims = [NSMutableArray array];

                // Gather this view 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 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 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 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];
}

Ответ 2

После изучения этой проблемы в течение некоторого времени с помощью консоли debug, Instruments и Reveal, я обнаружил следующее:

1) На симуляторе ошибка может быть воссоздана каждый раз, если используется шаблон профиля/автоматизации и добавляется следующий 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) На реальном устройстве (iPhone 5s, iOS 7.1) этот script никогда не вызывает ошибку. Я пробовал различные варианты для координат щелчка и задержки.

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 верны (альфа: 1 и кадр, как в обычной ситуации). Также _UINavigationBarBackIndicatorView выглядит не соответствует фактическим свойствам - он виден, хотя альфа - это 0.

Из этого я заключаю, что это ошибка симулятора и что вы даже не можете обнаружить из кода, что что-то не так.

Итак, @troop231 - вы на 100% уверены, что это также происходит на устройстве?

Ответ 3

Ключевые понятия

Отключить распознаватель жестов при нажатии контроллера просмотра и включить его при появлении представления.

Общее решение: подклассом

Вы можете подклассифицировать UINavigationController и UIViewController, чтобы предотвратить повреждение.

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
}

Проблема: слишком раздражает

  • Необходимо использовать MyNavigationController и MyViewController вместо UINavigationController и UIViewController.
  • Требуется подкласс для UITableViewController, UICollectionViewController и т.д.

Лучшее решение: метод Swizzling

Это можно сделать с помощью swizzling методов UINavigationController и UIViewController. Хотите узнать о методе swizzling, посетите здесь.

В примере ниже используется JRSwizzle, что облегчает процесс swizzling.

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

Быть простым: использовать Open Source

SwipeBack

SwipeBack делает это автоматически без какого-либо кода.

С CocoaPods просто добавьте строку ниже в Podfile. Вам не нужно писать какой-либо код. CocoaPods автоматически импортирует SwipeBack глобально.

pod 'SwipeBack'

Установите pod, и все сделано!