company_banner

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

    Поведение 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/

    FunCorp
    Разработка развлекательных сервисов

    Комментарии 5

      –7
      Стало хуже.

      Пусть лучше будет одинаково неправильно во всех приложениях, чем везде по-своему криво.
        +6
        Хотел показать не конеретную анимацию, а возможности к кастомизации. Но спасибо за мнение
        • НЛО прилетело и опубликовало эту надпись здесь
      +1
      Лучше не трогайте стандартные элементы UIKit, что работает сегодня, то не работает завтра с очередным обновлением iOS. К тому же, оно может работать абсолютно не так, как ожидаете на iPad или при перевороте.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое