UINavigationBar цветная анимация синхронизирована с push-анимацией

Я хочу добиться плавной анимации между видами с другим UINavigationBar фоновые цвета. Встроенные представления имеют тот же цвет фона, что и UINavigationBar и я хочу имитировать анимацию перехода push/pop как:

Я подготовил пользовательский переход:

class CustomTransition: NSObject, UIViewControllerAnimatedTransitioning {

    private let duration: TimeInterval
    private let isPresenting: Bool

    init(duration: TimeInterval = 1.0, isPresenting: Bool) {
        self.duration = duration
        self.isPresenting = isPresenting
    }

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return duration
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let container = transitionContext.containerView
        guard
            let toVC = transitionContext.viewController(forKey: .to),
            let fromVC = transitionContext.viewController(forKey: .from),
            let toView = transitionContext.view(forKey: .to),
            let fromView = transitionContext.view(forKey: .from)
        else {
            return
        }

        let rightTranslation = CGAffineTransform(translationX: container.frame.width, y: 0)
        let leftTranslation = CGAffineTransform(translationX: -container.frame.width, y: 0)

        toView.transform = isPresenting ? rightTranslation : leftTranslation

        container.addSubview(toView)
        container.addSubview(fromView)

        fromVC.navigationController?.navigationBar.backgroundColor = .clear
        fromVC.navigationController?.navigationBar.setBackgroundImage(UIImage.fromColor(color: .clear), for: .default)

        UIView.animate(
            withDuration: self.duration,
            animations: {
                fromVC.view.transform = self.isPresenting ? leftTranslation :rightTranslation
                toVC.view.transform = .identity
            },
            completion: { _ in
                fromView.transform = .identity
                toVC.navigationController?.navigationBar.setBackgroundImage(
                    UIImage.fromColor(color: self.isPresenting ? .yellow : .lightGray),
                    for: .default
                )
                transitionContext.completeTransition(true)
            }
        )
    }
}

И вернул его в UINavigationControllerDelegate Реализация метода:

func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return CustomTransition(isPresenting: operation == .push)
}

В то время как push-анимация работает довольно хорошо, поп-музыка не работает.

Вопросы:

  1. Почему после очистки цвета NavBar перед поп-анимацией он остается желтым?
  2. Есть ли лучший способ достичь моей цели? (navbar не может быть просто прозрачным все время, потому что это только часть потока)

Вот ссылка на мой тестовый проект на GitHub.

РЕДАКТИРОВАТЬ

Вот рисунок, представляющий полную картину обсуждаемой проблемы и желаемого эффекта:

3 ответа

Решение

Эти компоненты всегда очень сложно настроить. Я думаю, Apple хочет, чтобы системные компоненты выглядели и вели себя одинаково в каждом приложении, потому что это позволяет поддерживать общий пользовательский опыт во всей среде iOS.

Иногда проще реализовать собственные компоненты с нуля, чем пытаться настроить системные компоненты. Настройка часто может быть сложной, потому что вы не знаете наверняка, как компоненты разработаны внутри. В результате вам придется обрабатывать множество крайних случаев и справляться с ненужными побочными эффектами.

Тем не менее, я считаю, что у меня есть решение для вашей ситуации. Я раздвоил ваш проект и реализовал поведение, которое вы описали. Вы можете найти мою реализацию на GitHub. Увидеть animation-implementation ветка.


UINavigationBar

Основная причина поп-анимации не работает должным образом, это то, что UINavigationBar имеет собственную внутреннюю логику анимации. когда UINavigationController's изменения стека, UINavigationController говорит UINavigationBar изменить UINavigationItems, Итак, сначала нам нужно отключить системную анимацию для UINavigationItems, Это может быть сделано путем подклассов UINavigationBar:

class CustomNavigationBar: UINavigationBar {
   override func pushItem(_ item: UINavigationItem, animated: Bool) {
     return super.pushItem(item, animated: false)
   }

   override func popItem(animated: Bool) -> UINavigationItem? {
     return super.popItem(animated: false)
   }
}

затем UINavigationController должен быть инициализирован с CustomNavigationBar:

let nc = UINavigationController(navigationBarClass: CustomNavigationBar.self, toolbarClass: nil)

UINavigationController


Поскольку существует необходимость сохранять анимацию плавной и синхронизированной между UINavigationBar и представил UIViewControllerнам нужно создать пользовательский объект анимации перехода для UINavigationController и использовать CoreAnimation с CATransaction,

Пользовательский переход

Ваша реализация переходного аниматора практически идеальна, но, с моей точки зрения, некоторые детали были упущены. В статье " Настройка анимации перехода" вы можете найти больше информации. Также, пожалуйста, обратите внимание на методы комментариев в UIViewControllerContextTransitioning протокол.

Итак, моя версия push-анимации выглядит следующим образом:

func animatePush(_ transitionContext: UIViewControllerContextTransitioning) {
  let container = transitionContext.containerView

  guard let toVC = transitionContext.viewController(forKey: .to),
    let toView = transitionContext.view(forKey: .to) else {
      return
  }

  let toViewFinalFrame = transitionContext.finalFrame(for: toVC)
  toView.frame = toViewFinalFrame
  container.addSubview(toView)

  let viewTransition = CABasicAnimation(keyPath: "transform")
  viewTransition.duration = CFTimeInterval(self.duration)
  viewTransition.fromValue = CATransform3DTranslate(toView.layer.transform, container.layer.bounds.width, 0, 0)
  viewTransition.toValue = CATransform3DIdentity

  CATransaction.begin()
  CATransaction.setAnimationDuration(CFTimeInterval(self.duration))
  CATransaction.setCompletionBlock = {
      let cancelled = transitionContext.transitionWasCancelled
      if cancelled {
          toView.removeFromSuperview()
      }
      transitionContext.completeTransition(cancelled == false)
  }
  toView.layer.add(viewTransition, forKey: nil)
  CATransaction.commit()
}

Реализация поп-анимации практически одинакова. Единственная разница в CABasicAnimation значения fromValue а также toValue свойства.

UINavigationBar анимация

Для того, чтобы оживить UINavigationBar мы должны добавить CATransition анимация на UINavigationBar слой:

let transition = CATransition()
transition.duration = CFTimeInterval(self.duration)
transition.type = kCATransitionPush
transition.subtype = self.isPresenting ? kCATransitionFromRight : kCATransitionFromLeft
toVC.navigationController?.navigationBar.layer.add(transition, forKey: nil)

Код выше будет анимировать весь UINavigationBar, Для того, чтобы анимировать только фон UINavigationBar нам нужно получить фоновый вид из UINavigationBar, А вот и хитрость: первое подвид UINavigationBar является _UIBarBackground представление (это могло быть исследовано, используя Xcode Debug View Hierarchy). Точный класс не важен в нашем случае, достаточно, чтобы он был наследником UIView, Наконец, мы могли бы добавить наш анимационный переход на _UIBarBackgroundВид слоя прямо:

let backgroundView = toVC.navigationController?.navigationBar.subviews[0]
backgroundView?.layer.add(transition, forKey: nil)

Я хотел бы отметить, что мы прогнозируем, что первое подпредставление является фоновым представлением. Иерархия представления может быть изменена в будущем, просто имейте это в виду.

Важно добавить обе анимации в одну CATransactionпотому что в этом случае эти анимации будут выполняться одновременно.

Вы могли бы настроить UINavigationBar цвет фона в viewWillAppear метод каждого контроллера представления.

Вот как выглядит финальная анимация:

Надеюсь, это поможет.

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

Редактировать: Вы также можете получить "белый контент", ограничив containerView под панелью навигации. В примере кода я это сделал. Толчок случайным образом выбирает цвет и дает виду контейнера случайным образом белый или прозрачный цвет. Вы увидите, что все сценарии в вашем GIF покрыты этим примером.

Попробуйте это на детской площадке:

import UIKit
import PlaygroundSupport

class MyViewController : UIViewController {
  var containerView: UIView!

  override func loadView() {
    let view = UIView()
    view.backgroundColor = .white

    containerView = UIView()
    containerView.backgroundColor = .white
    containerView.translatesAutoresizingMaskIntoConstraints = false

    view.addSubview(containerView)
    containerView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor).isActive = true
    containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
    containerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
    containerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true

    let button = UIButton()
    button.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
    button.setTitle("push", for: .normal)
    button.setTitleColor(.black, for: .normal)
    button.addTarget(self, action: #selector(push), for: .touchUpInside)
    containerView.addSubview(button)
    self.view = view
  }

  @objc func push() {
    let colors: [UIColor] = [.yellow, .red, .blue, .purple, .gray, .darkGray, .green]
    let controller = MyViewController()
    controller.title = "Second"

    let randColor = Int(arc4random()%UInt32(colors.count))
    controller.view.backgroundColor = colors[randColor]

    let clearColor: Bool = (arc4random()%2) == 1
    controller.containerView.backgroundColor = clearColor ? .clear: .white
    navigationController?.pushViewController(controller, animated: true)
  }
}
// Present the view controller in the Live View window

let controller = MyViewController()
controller.view.backgroundColor = .white
let navController = UINavigationController(rootViewController: controller)
navController.navigationBar.setBackgroundImage(UIImage(), for: .default)
controller.title = "First"


PlaygroundPage.current.liveView = navController

Удалите этот код из вашего проекта:

toVC.navigationController?.navigationBar.setBackgroundImage(
        UIImage.fromColor(color: self.isPresenting ? .yellow : .lightGray),
        for: .default
)
Другие вопросы по тегам