247,75
Рейтинг
FunCorp
Разработка развлекательных сервисов
30 сентября

Navigation bar и анимация перехода

Блог компании FunCorpПрограммированиеРазработка под iOSXcodeSwift

Поведение UINavigationBar при переходе по стеку может показаться непредсказуемым и часто забагованным. Но, на самом деле, так и есть! Эта статья призвана освежить знания о принципах работы и показать возможности кастомизации поведения.

Немного общей теории

Если вы хорошо осведомлены, смело пролистывайте непосредственно к анимации.

  1. UINavigationBar – это view. Как правило, его положением управляет UINavigationController, но, так же как и другие view, его можно использовать самостоятельно.

  2. UINavigationItem – это класс, описывающий состояние (похожее на viewModel) для конфигурации UINavigationBar. Просто класс со свойствами, которые будут переданы в UINavigationBar.

  3. UINavigationBar содержит массив [UINavigationItem]. С помощью методов pushItem, popItem и setItems можно анимировать переход одного состояния UINavigationBar в другое.

  4. Каждый UIViewController содержит UINavigationItem. UINavigationController сам управляет добавлением этого свойства в стек item-ов UINavigationBar-а.

Более подробно можно почитать вот здесь:

Также приведу ссылку на полезную схему полного жизненного цикла с UINavigationBar-ом. Это поможет лучше понимать его поведение:

Проблема с изменением высоты

UINavigationBar может иметь разную высоту. Это зависит от следующих параметров:

  • Наличие или отсутствие prompt (текст над заголовком)

  • largeTitleDisplayMode управляет показом большого или стандартного заголовка

Так как UINavigationBar – это одна view, а контроллеров – пара, при переходе анимировано меняется в том числе и высота, что часто урезает контент.

Для наглядности покрасим навбар: розовый – это navigationBar.backgroundColor, а зелёный – navigationBar.barTintColor.

Одно из решений – использовать прозрачный backgroundColor. Кстати, backgroundColor – это цвет именно view и он не учитывает смещение на статус-бар.

При переходе между контроллерами с имеющимся prompt-ом такой трюк уже не сработает.

К сожалению, контроля над transition анимацией в самом UINavigationBar мы не имеем. А при использовании UIViewControllerAnimatedTransitioning потеряем анимацию в UINavigationBar.

План у нас следующий: добавить адаптацию контента, используя системную анимацию перехода.

Создадим несколько контроллеров и используем safeArea для лейаута контента. В последующем смена additionalSafeAreaInsets у UIViewController-а позволит его анимировать:

contentView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true

Опишем наследника UINavigationController и добавим реализацию протокола UINavigationControllerDelegate. Это позволит отлавливать событие показа контроллеров, и именно тут будет модифицироваться анимация.

class NavigationController: UINavigationController, UINavigationControllerDelegate { }

Не забудем присвоить делегат самому UINavigationController-у.

navigationController.delegate = navigationController

Далее самое интересное. Ловим UIViewController в методе navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) перед появлением и добавим в transitionCoordinator анимацию для safeArea.

guard let fromViewController = viewController.transitionCoordinator?.viewController(forKey: .from) 

else { return }

// Определяем тип анимации. Если контроллера нет в стеке, значит, это pop

let isPopped = !navigationController.viewControllers.contains(fromViewController)

 

// Добавляем анимацию, которая будет выполнятся одновременно с системной

viewController.transitionCoordinator?.animate { context in

    guard let from = context.viewController(forKey: .from),

          let to = context.viewController(forKey: .to)

    else { return }

    

    // Установка начального состояния

    // Перед анимацией задать параметры не выйдет, потому что safeArea изменится

    // и расчёты могут быть неверными

    UIView.setAnimationsEnabled(false)

    let diff = to.view.safeAreaInsets.top - from.view.safeAreaInsets.top

    // Выравниваем контент первого контроллера относительно второго

    to.additionalSafeAreaInsets.top = -diff

    to.view.layoutIfNeeded()

    UIView.setAnimationsEnabled(true)

    

    // Анимируем safeArea

    to.additionalSafeAreaInsets.top = 0

    to.view.layoutIfNeeded()

    

    guard isPopped else { return }

 

    // Изменение фрейма только для pop-анимации

    // так как в этом случае additionalSafeAreaInsets не анимируется

    from.view.frame.origin.y = diff

    from.view.frame.size.height += max(0, -diff)

    

} completion: { context in

    guard let from = context.viewController(forKey: .from),

          let to = context.viewController(forKey: .to)

    else { return }

    

    from.additionalSafeAreaInsets.top = 0

    to.additionalSafeAreaInsets.top = 0

}

Посмотрим, что вышло:

Заключение

Безусловно показанный пример – это не универсальный код. Скорее всего, вы ещё сотню раз столкнётесь с проблемами в UINavigationBar. Суть в том, чтобы показать, как понимание основ и принципов может облегчить разработку.

Если у вас есть свои интересные решения, буду рад обсудить их в комментариях :)

Источники

Твиттер: Rtishchev Evgenii https://twitter.com/katleta3000/status/1259400743771156480

https://stackoverflow.com/questions/39515313/animate-navigation-bar-bartintcolor-change-in-ios10-not-working

Лучшие практики для navigation bar: https://www.programmersought.com/article/1594185256/

Теги:uinavigationcontrolleruinavigationcontrollerdelegateuinavigationitemuinavigationbar
Хабы: Блог компании FunCorp Программирование Разработка под iOS Xcode Swift
+27
4,1k 34
Комментарии 5
Senior iOS Developer
от 200 000 ₽FunCorpМосква
Senior Android Developer
от 200 000 ₽FunCorpМосква
Senior Kotlin Backend Developer
от 200 000 ₽FunCorpМосква
Senior QA Backend Engineer
FunCorpМосква
Лучшие публикации за сутки
Информация
Дата основания

17 января 2005

Местоположение

Кипр

Сайт

fun.co

Численность

51–100 человек

Дата регистрации

28 августа 2013

Блог на Хабре