Quiero lograr una animación fluida entre las vistas con diferentes colores de fondo UINavigationBar. Las vistas incrustadas tienen el mismo color de fondo que UINavigationBar y quiero imitar la animación de transición push / pop como:

enter image description here

He preparado una transición personalizada:

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

Y lo devolvió en la implementación del método UINavigationControllerDelegate:

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

Mientras que la animación push funciona bastante bien, el pop no lo hace.

enter image description here

Preguntas:

  1. ¿Por qué después de borrar el color NavBar antes de la animación pop, permanece amarillo?
  2. ¿Hay alguna forma mejor de lograr mi objetivo? (la barra de navegación no puede ser transparente todo el tiempo porque es solo una parte del flujo)

Aquí está el enlace a mi proyecto de prueba en GitHub.

EDITAR

Aquí está el gif que presenta la imagen completa del tema discutido y el efecto deseado:

enter image description here

2
Fayer 17 ene. 2018 a las 03:24

3 respuestas

La mejor respuesta

Estos componentes siempre son muy difíciles de personalizar. Creo que Apple quiere que los componentes del sistema se vean y se comporten por igual en todas las aplicaciones, ya que permite mantener la experiencia del usuario compartida en todo el entorno iOS.

A veces, es más fácil implementar sus propios componentes desde cero en lugar de tratar de personalizar los del sistema. La personalización a menudo puede ser complicada porque no sabes con seguridad cómo se diseñan los componentes en su interior. Como resultado, debe manejar muchos casos extremos y lidiar con efectos secundarios innecesarios.

Sin embargo, creo que tengo una solución para su situación. Bifurqué su proyecto e implementé el comportamiento que había descrito. Puede encontrar mi implementación en GitHub. Ver rama animation-implementation.


UINavigationBar

La causa raíz de la animación pop no funciona correctamente, es que UINavigationBar tiene su propia lógica de animación interna. Cuando UINavigationController's la pila cambia, UINavigationController le dice a UINavigationBar que cambie UINavigationItems. Entonces, al principio, necesitamos deshabilitar la animación del sistema para UINavigationItems. Podría hacerse subclasificando 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)
   }
}

Entonces UINavigationController debe inicializarse con CustomNavigationBar:

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

UINavigationController


Dado que existe el requisito de mantener la animación suave y sincronizada entre UINavigationBar y presentada UIViewController, necesitamos crear un objeto de animación de transición personalizado para UINavigationController y usar CoreAnimation con {{X4} }.

Transición personalizada

Su implementación del animador de transición es casi perfecta, pero desde mi punto de vista se perdieron pocos detalles. En el artículo Personalizando las animaciones de transición puede encontrar más información . Además, preste atención a los comentarios de métodos en el protocolo UIViewControllerContextTransitioning.

Entonces, mi versión de la animación push se ve de la siguiente manera:

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()
}

La implementación de animación pop es casi la misma. La única diferencia en los valores CABasicAnimation de las propiedades fromValue y toValue.

UINavigationBar animation

Para animar UINavigationBar tenemos que agregar animación CATransition en la capa 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)

El código anterior animará todo UINavigationBar. Para animar solo el fondo de UINavigationBar necesitamos recuperar la vista de fondo de UINavigationBar. Y aquí está el truco: la primera subvista de UINavigationBar es la vista _UIBarBackground (podría explorarse usando la Jerarquía de vista de depuración de Xcode). La clase exacta no es importante en nuestro caso, es suficiente que sea sucesora de UIView. Finalmente, podríamos agregar nuestra transición de animación en la capa de vista de _UIBarBackground directamente:

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

Me gustaría señalar que estamos haciendo predicciones de que la primera subvista es una vista de fondo. La jerarquía de la vista podría cambiarse en el futuro, solo tenga esto en cuenta.

Es importante agregar ambas animaciones en una CATransaction, porque en este caso estas animaciones se ejecutarán simultáneamente.

Puede configurar el color de fondo UINavigationBar en el método viewWillAppear de cada controlador de vista.

Así es como se ve la animación final:

enter image description here

Espero que esto ayude.

2
Alex D. 25 ene. 2018 a las 20:31

Elimine este código de su proyecto:

toVC.navigationController?.navigationBar.setBackgroundImage(
        UIImage.fromColor(color: self.isPresenting ? .yellow : .lightGray),
        for: .default
)
0
Datt Patel 17 ene. 2018 a las 06:30

La forma en que lo haría es haciendo que el controlador de navegación sea completamente transparente. De esta manera, la animación del controlador de vista contenido debería dar el efecto que desea.

Editar: También puede obtener "contenido en blanco" al tener una vista de contenedor restringida debajo de la barra de navegación. En el código de muestra hice eso. El empuje selecciona aleatoriamente un color y le da al contenedor una vista aleatoriamente blanca o clara. Verá que todos los escenarios en su gif están cubiertos por este ejemplo.

Prueba esto en el patio de recreo:

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
1
Giuseppe Lanza 23 ene. 2018 a las 11:44