Ошибка заголовка панели навигации с InteractivePopGestureRecognizer

У меня странная проблема с заголовком UINavigationBar в приложении, когда interactivePopGestureRecognizer вступает в игру. Я сделал демонстрационное приложение, чтобы продемонстрировать эту ошибку.

Настраивать:

  • RootViewController - это UINavigationController.
  • FirstViewController панель навигации скрыта, а interactivePopGestureRecognizer.enabled = NO;
  • Second и ThirdViewControllers имеют видимую панель навигации и активированное всплывающее окно.

Ошибка:

Ошибка возникает при возврате от второго к первому представлению с использованием вытягивания. Если вы потянете второе представление наполовину, а затем вернетесь ко второму представлению, в заголовке навигации будет отображаться «Второй вид» (как и ожидалось). Но когда вы перейдете к третьему представлению, заголовок не изменится на «Третье представление». А затем при нажатии кнопки возврата в третьем представлении навигационная панель испортится.

Пожалуйста, ознакомьтесь с моим демонстрационным приложением. Любая помощь, объясняющая, почему возникает эта ошибка, будет оценена. Спасибо!


person aksh1t    schedule 24.04.2014    source источник
comment
Я предоставил три решения (см. Мой ответ ниже) в виде вилки для вашего примера на github и выпустил запрос на перенос, чтобы вы могли их опробовать. После извлечения (и извлечения на свою машину) используйте git checkout, чтобы проверить каждое из трех решений и попробовать их по очереди, чтобы вы могли изучить код и посмотреть, как они работают.   -  person matt    schedule 26.04.2014


Ответы (5)


Убрать красные сельди

Во-первых, ваш пример можно сильно упростить. Вам следует удалить весь viewDidLoad материал, так как это полный отвлекающий маневр и только усложняет проблему. Вы не должны возиться с делегатом распознавателя жестов при каждом изменении контроллера представления; и включение и выключение распознавателя поп-жестов не имеет отношения к этому примеру (он включен по умолчанию, и его следует просто оставить включенным для этого примера). Так что удалите такие вещи во всех трех контроллерах представления:

- (void)viewDidLoad {
    [super viewDidLoad];
    if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
        self.navigationController.interactivePopGestureRecognizer.enabled = NO;
        self.navigationController.interactivePopGestureRecognizer.delegate = self;
    }
}

(Не удаляйте код, устанавливающий self.title, хотя вы могли бы упростить задачу, сделав это в файле xib для каждого контроллера представления.)

Вы также можете полностью избавиться от других неиспользуемых методов, таких как методы init... и методы оповещения о памяти.

Кстати, еще одна проблема заключается в том, что вы забыли вызвать super в своих реализациях viewWillAppear:. Это требуется от вас. Я не думаю, что это влияет на ошибку, но лучше соблюдать все правила, прежде чем пытаться отследить эти вещи.

Теперь ошибка все еще возникает, но у нас есть гораздо более простой код, поэтому мы можем начать изолировать проблему.

Как работает поп-жест

Так в чем причина проблемы? Я думаю, что наиболее очевидный способ понять это - понять, как работает поп-жест. Это интерактивная анимация перехода контроллера представления. Правильно - это анимация. Это работает так, что всплывающая анимация (слайд слева) прикрепляется к слою супервизора, но с speed равным 0, так что фактически она не запускается. По мере выполнения жеста timeOffset слоя постоянно обновляется, так что появляется соответствующий «кадр» анимации. Таким образом, выглядит так, как будто вы перетаскиваете представление, но это не так; вы просто делаете жест, а анимация выполняется с той же скоростью и в той же степени. Я объяснил этот механизм в этом ответе: https://stackoverflow.com/a/22677298/341994

Наиболее важно (обратите внимание на эту часть), если жест отброшен в середине (что почти наверняка будет), принимается решение относительно того, завершен ли жест более чем наполовину, и на основании этого либо анимация быстро воспроизводится до конца (т.е. speed устанавливается на что-то вроде 3) или анимация запускается в обратном направлении до начала (т.е. speed устанавливается на что-то вроде -3).

Решения и почему они работают

Теперь поговорим об ошибке. Здесь есть два осложнения, с которыми вы случайно столкнулись:

  • Когда начинается всплывающая анимация и всплывающий жест, viewWillAppear: вызывается для предыдущего контроллера представления, даже если представление в конечном итоге может не появиться (потому что это интерактивный жест, и этот жест может быть отменен). Это может быть серьезной проблемой, если вы привыкли к предположению, что за viewWillAppear: всегда следует представление, фактически захватывающее экран (и вызываемое viewDidAppear:), потому что это ситуация, в которой эти вещи могут не произойдет. (Как Apple говорит в видеороликах WWDC 2013, «представление появится» на самом деле означает «представление может появиться».)

  • Есть вторичный набор анимаций, а именно все, что связано с панелью навигации - смена заголовка (он должен исчезнуть из поля зрения) и, в данном случае, смена между не скрытым и скрытым. Среда выполнения пытается согласовать вторичный набор анимаций с анимацией скользящего просмотра. Но вы усложнили это, вызвав нет анимации, когда полоса скрыта или отображается.

Таким образом, как вам уже было сказано, одно решение - изменить animated:NO на animated:YES во всем коде. Таким образом, отображение и скрытие панели навигации упорядочивается как часть анимации. Следовательно, когда жест отменяется и анимация выполняется в обратном направлении к началу, отображение / скрытие навигации также выполняется в обратном направлении к началу - теперь эти две вещи остаются согласованными.

Но что, если вы действительно не хотите вносить это изменение? Что ж, другое решение - полностью изменить viewWillAppear: на viewDidAppear:. Как я уже сказал, viewWillAppear: вызывается в начале анимации, даже если жест не будет завершен, что приводит к выходу из строя. Но viewDidAppear: вызывается только в том случае, если жест завершен (не отменен) и когда анимация уже закончилась.

Какое из этих двух решений я предпочитаю? Никто из них! Они оба заставляют вас вносить изменения, которых вы не хотите. Мне кажется, что реальным решением является использование координатора перехода.

Координатор перехода

Координатор перехода - это объект, предоставляемый системой для этой самой цели, то есть для обнаружения того, что мы участвуем в интерактивном переходе, и для того, чтобы вести себя по-разному в зависимости от того, отменен он или нет.

Сосредоточьтесь только на реализации viewWillAppear: в OneViewController. Здесь все запутывается. Когда вы находитесь в TwoViewController и запускаете жест панорамирования слева, вызывается viewWillAppear: OneViewController. Но затем вы отменяете жест, не завершая его. Только в этом одном случае вы не хотите, чтобы не выполняли то, что делали в viewWillAppear: OneViewController. И это именно то, что позволяет вам делать координатор перехода.

Итак, это переработанная версия viewWillAppear: в OneViewController. Это устраняет проблему без необходимости вносить какие-либо другие изменения:

-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    id<UIViewControllerTransitionCoordinator> tc = self.transitionCoordinator;
    if (tc && [tc initiallyInteractive]) {
        [tc notifyWhenInteractionEndsUsingBlock:
         ^(id<UIViewControllerTransitionCoordinatorContext> context) {
             if ([context isCancelled]) {
                 // do nothing!
             } else { // not cancelled, do it
                 [self.navigationController setNavigationBarHidden:YES animated:NO];
             }
         }];
    } else { // not interactive, do it
        [self.navigationController setNavigationBarHidden:YES animated:NO];
    }
}
person matt    schedule 26.04.2014
comment
Вау, это потрясающе подробный ответ. Именно то, что я искал! Спасибо, @matt! - person aksh1t; 28.04.2014
comment
Используя координатор перехода, мы исправили беспорядок, который у нас был в нашем приложении при перемещении экранов форм, на которых была скрыта панель навигации, и тех, на которых не было - person Avi Cohen; 02.12.2014
comment
Я бы хотел объединить этот ответ с stackoverflow.com/a/2406167/354018, но я до сих пор не понимаю ошибка при отмене всплывающего жеста до завершения анимации скрытия панели навигации (см. мой комментарий в ответе по ссылке). Я застрял, есть идеи, как это сделать? - person fabb; 24.04.2015
comment
любая помощь здесь будет отличной @matt: stackoverflow.com/questions/48954413/ - person user7097242; 23.02.2018

Исправление простое, но на данный момент у меня нет никаких объяснений, почему это происходит.

Один из ваших OneViewController измените свой viewWillAppear на,

-(void)viewWillAppear:(BOOL)animated{
   // [self.navigationController setNavigationBarHidden:YES animated:NO];
    self.navigationController.navigationBar.hidden = YES;
}

и во втором и третьем контроллерах представления измените его на,

 -(void)viewWillAppear:(BOOL)animated{
         //[self.navigationController setNavigationBarHidden:NO animated:NO];
         self.navigationController.navigationBar.hidden = NO;
    }

Странно, но это решит проблему, когда мы напрямую используем скрытое свойство UINavigationBar.

person rustylepord    schedule 26.04.2014
comment
Использование метода set с анимированным параметром YES также решит эту проблему! [self.navigationController setNavigationBarHidden: ДА, анимировано: ДА]; - person Sergey Neskoromny; 26.04.2014
comment
В этом нет ничего странного, панель навигации разделена между представленными контроллерами представлений. Сроки вид будет появляться / вид исчезнет методы правильно, может создать желаемый эффект. - person Leo Natan; 26.04.2014
comment
Лучше использовать анимированный параметр. - person Mr. Ming; 25.02.2016
comment
Это не лучший подход. При таком подходе, когда я возвращаюсь к предыдущему контроллеру представления, на середине я вижу, что панель исчезает и частично появляется. Если свойство отображения / скрытия панели навигации изменено в обоих контроллерах. - person Ammar Mujeeb; 26.09.2016

Я не знаю, как сделать так, чтобы «FirstViewController скрыл панель навигации».

У меня такая же проблема, и я исправил ее, заменив

self.navigationController.navigationBarHidden = YES / NO;

by

[self.navigationController setNavigationBarHidden:YES / NO animated:animated];
person Mr. Ming    schedule 24.02.2016

Я отказался от попыток заставить эту работу использовать мой собственный распознаватель смахиваний, который открывает стек навигации:

override func viewDidLoad() {
    super.viewDidLoad()

    // disable system swipe back gesture and add our own
    navigationController?.interactivePopGestureRecognizer?.enabled = false
    let swipeBackGestureRecognizer = UISwipeGestureRecognizer(target: self, action: "swipeBackAction:")
    swipeBackGestureRecognizer.direction = UISwipeGestureRecognizerDirection.Right
    tableView.addGestureRecognizer(swipeBackGestureRecognizer)
}

func swipeBackAction(sender: UISwipeGestureRecognizer) {

    navigationController?.popViewControllerAnimated(true)
}
  • Отключить систему interactivePopGestureRecognizer
  • Создайте свой собственный UISwipeGestureRecognizer с правильным направлением
  • Всплывает анимированный стек навигации, когда он обнаруживает смахивание
person Michael Peterson    schedule 10.03.2016

Вот что исправило для меня (Swift)

Контроллер 1-го вида:

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    self.navigationController?.setNavigationBarHidden(true, animated: animated)
}

Контроллеры 2-го и 3-го вида:

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    self.navigationController?.setNavigationBarHidden(false, animated: animated)
}
person AmitP    schedule 16.05.2016