Compartir una imagen entre dos viewControllers durante una animación de transición

He encontrado transiciones geniales entre viewControllers desde que el protocolo UIViewControllerAnimatedTransitioning estuvo disponible en IOS 7. Recientemente noté una particularmente interesante en la aplicación IOS de Intacart.

Aquí está la animación de la que estoy hablando en cámara lenta: https://www.dropbox.com/s/p2hxj45ycq18i3l/Video%20Oct%2015%2C%207%2023%2059%20PM.mov?dl=0

Primero pensé que era similar a lo que el autor describe en este tutorial, con algunas animaciones adicionales de desvanecimiento y desvanecimiento: http://www.raywenderlich.com/113845/ios-animation-tutorial-custom-view- controlador-presentación-transiciones

Pero si lo miras detenidamente, parece que la imagen del producto pasa de una transición a otra de los viewControllers a medida que el primer viewController se desvanece. La razón por la que creo que hay dos viewControllers se debe a que cuando desliza la nueva vista hacia abajo, aún puede ver la vista original detrás de ella sin cambios de diseño.

Tal vez dos viewControllers realmente tienen la imagen del producto (no se desvaneció) y de alguna manera están animando al mismo tiempo con perfecta precisión y uno de ellos se desvanece a medida que el otro se desvanece.

¿Qué crees que realmente está pasando allí?

¿Cómo es posible progtwigr una animación de transición tal que parezca que una imagen se comparte entre dos viewControllers?

Probablemente sean dos vistas diferentes y una vista de instantánea animada. De hecho, esta es exactamente la razón por la que se inventaron las vistas instantáneas.

Así es como lo hago en mi aplicación. Observe el movimiento del rectángulo rojo a medida que la vista presentada se desliza hacia arriba y hacia abajo:

enter image description here

Parece que la vista roja está saliendo del primer controlador de visualización e ingresando al segundo controlador de vista, pero es solo una ilusión. Si tiene una animación de transición personalizada, puede agregar vistas adicionales durante la transición. Así que creo una vista de instantánea que se parece a la primera vista, oculto la primera vista real y muevo la vista de instantánea, y luego elimino la vista de instantánea y muestro la segunda vista real.

Lo mismo aquí (un truco tan bueno que lo uso en muchas aplicaciones): parece que el título se ha soltado de la primera celda de vista de tabla de controlador de vista y se deslizó hasta el segundo controlador de vista, pero es solo una instantánea ver:

enter image description here

Esto es lo que hicimos para lograr una captura de pantalla flotante de la vista durante la transición animada (Swift 4):

Idea detrás:

  1. Los controladores de vista de origen y destino cumplen con el protocolo InterViewAnimatable . Estamos usando este protocolo para encontrar vistas de origen y destino.
  2. Luego creamos instantáneas de esas vistas y las ocultamos. En cambio, en la misma posición se muestran instantáneas.
  3. Luego animamos instantáneas.
  4. Al final de la transición, mostramos la vista de destino.

Como resultado, parece que la vista de origen está volando hacia el destino.

Archivo: InterViewAnimation.swift

 // TODO: In case of multiple views, add another property which will return some unique string (identifier). protocol InterViewAnimatable { var targetView: UIView { get } } class InterViewAnimation: NSObject { var transitionDuration: TimeInterval = 0.25 var isPresenting: Bool = false } extension InterViewAnimation: UIViewControllerAnimatedTransitioning { func transitionDuration(using: UIViewControllerContextTransitioning?) -> TimeInterval { return transitionDuration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let containerView = transitionContext.containerView guard let fromVC = transitionContext.viewController(forKey: .from), let toVC = transitionContext.viewController(forKey: .to) else { transitionContext.completeTransition(false) return } guard let fromTargetView = targetView(in: fromVC), let toTargetView = targetView(in: toVC) else { transitionContext.completeTransition(false) return } guard let fromImage = fromTargetView.caSnapshot(), let toImage = toTargetView.caSnapshot() else { transitionContext.completeTransition(false) return } let fromImageView = ImageView(image: fromImage) fromImageView.clipsToBounds = true let toImageView = ImageView(image: toImage) toImageView.clipsToBounds = true let startFrame = fromTargetView.frameIn(containerView) let endFrame = toTargetView.frameIn(containerView) fromImageView.frame = startFrame toImageView.frame = startFrame let cleanupClosure: () -> Void = { fromTargetView.isHidden = false toTargetView.isHidden = false fromImageView.removeFromSuperview() toImageView.removeFromSuperview() } let updateFrameClosure: () -> Void = { // https://stackoverflow.com/a/27997678/1418981 // In order to have proper layout. Seems mostly needed when presenting. // For instance during presentation, destination view does'n account navigation bar height. toVC.view.setNeedsLayout() toVC.view.layoutIfNeeded() // Workaround wrong origin due ongoing layout process. let updatedEndFrame = toTargetView.frameIn(containerView) let correctedEndFrame = CGRect(origin: updatedEndFrame.origin, size: endFrame.size) fromImageView.frame = correctedEndFrame toImageView.frame = correctedEndFrame } let alimationBlock: (() -> Void) let completionBlock: ((Bool) -> Void) fromTargetView.isHidden = true toTargetView.isHidden = true if isPresenting { guard let toView = transitionContext.view(forKey: .to) else { transitionContext.completeTransition(false) assertionFailure() return } containerView.addSubviews(toView, toImageView, fromImageView) toView.frame = CGRect(origin: .zero, size: containerView.bounds.size) toView.alpha = 0 alimationBlock = { toView.alpha = 1 fromImageView.alpha = 0 updateFrameClosure() } completionBlock = { _ in let success = !transitionContext.transitionWasCancelled if !success { toView.removeFromSuperview() } cleanupClosure() transitionContext.completeTransition(success) } } else { guard let fromView = transitionContext.view(forKey: .from) else { transitionContext.completeTransition(false) assertionFailure() return } containerView.addSubviews(toImageView, fromImageView) alimationBlock = { fromView.alpha = 0 fromImageView.alpha = 0 updateFrameClosure() } completionBlock = { _ in let success = !transitionContext.transitionWasCancelled if success { fromView.removeFromSuperview() } cleanupClosure() transitionContext.completeTransition(success) } } // TODO: Add more precise animation (ie Keyframe) if isPresenting { UIView.animate(withDuration: transitionDuration, delay: 0, options: .curveEaseIn, animations: alimationBlock, completion: completionBlock) } else { UIView.animate(withDuration: transitionDuration, delay: 0, options: .curveEaseOut, animations: alimationBlock, completion: completionBlock) } } } extension InterViewAnimation { private func targetView(in viewController: UIViewController) -> UIView? { if let view = (viewController as? InterViewAnimatable)?.targetView { return view } if let nc = viewController as? UINavigationController, let vc = nc.topViewController, let view = (vc as? InterViewAnimatable)?.targetView { return view } return nil } } 

Uso:

 let sampleImage = UIImage(data: try! Data(contentsOf: URL(string: "https://placekitten.com/320/240")!)) class InterViewAnimationMasterViewController: StackViewController { private lazy var topView = View().autolayoutView() private lazy var middleView = View().autolayoutView() private lazy var bottomView = View().autolayoutView() private lazy var imageView = ImageView(image: sampleImage).autolayoutView() private lazy var actionButton = Button().autolayoutView() override func setupHandlers() { actionButton.setTouchUpInsideHandler { [weak self] in let vc = InterViewAnimationDetailsViewController() let nc = UINavigationController(rootViewController: vc) nc.modalPresentationStyle = .custom nc.transitioningDelegate = self vc.handleClose = { [weak self] in self?.dismissAnimated() } // Workaround for not up to date laout during animated transition. nc.view.setNeedsLayout() nc.view.layoutIfNeeded() vc.view.setNeedsLayout() vc.view.layoutIfNeeded() self?.presentAnimated(nc) } } override func setupUI() { stackView.addArrangedSubviews(topView, middleView, bottomView) stackView.distribution = .fillEqually bottomView.addSubviews(imageView, actionButton) topView.backgroundColor = UIColor.red.withAlphaComponent(0.5) middleView.backgroundColor = UIColor.green.withAlphaComponent(0.5) bottomView.layoutMargins = UIEdgeInsets(horizontal: 15, vertical: 15) bottomView.backgroundColor = UIColor.yellow.withAlphaComponent(0.5) actionButton.title = "Tap to perform Transition!" actionButton.contentEdgeInsets = UIEdgeInsets(dimension: 8) actionButton.backgroundColor = .magenta imageView.layer.borderWidth = 2 imageView.layer.borderColor = UIColor.magenta.withAlphaComponent(0.5).cgColor } override func setupLayout() { var constraints = LayoutConstraint.PinInSuperView.atCenter(imageView) constraints += [ LayoutConstraint.centerX(viewA: bottomView, viewB: actionButton), LayoutConstraint.pinBottoms(viewA: bottomView, viewB: actionButton) ] let size = sampleImage?.size.scale(by: 0.5) ?? CGSize() constraints += LayoutConstraint.constrainSize(view: imageView, size: size) NSLayoutConstraint.activate(constraints) } } extension InterViewAnimationMasterViewController: InterViewAnimatable { var targetView: UIView { return imageView } } extension InterViewAnimationMasterViewController: UIViewControllerTransitioningDelegate { func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { let animator = InterViewAnimation() animator.isPresenting = true return animator } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { let animator = InterViewAnimation() animator.isPresenting = false return animator } } class InterViewAnimationDetailsViewController: StackViewController { var handleClose: (() -> Void)? private lazy var imageView = ImageView(image: sampleImage).autolayoutView() private lazy var bottomView = View().autolayoutView() override func setupUI() { stackView.addArrangedSubviews(imageView, bottomView) stackView.distribution = .fillEqually imageView.contentMode = .scaleAspectFit imageView.layer.borderWidth = 2 imageView.layer.borderColor = UIColor.purple.withAlphaComponent(0.5).cgColor bottomView.backgroundColor = UIColor.blue.withAlphaComponent(0.5) navigationItem.leftBarButtonItem = BarButtonItem(barButtonSystemItem: .done) { [weak self] in self?.handleClose?() } } } extension InterViewAnimationDetailsViewController: InterViewAnimatable { var targetView: UIView { return imageView } } 

Extensiones reutilizables:

 extension UIView { // https://medium.com/@joesusnick/a-uiview-extension-that-will-teach-you-an-important-lesson-about-frames-cefe1e4beb0b public func frameIn(_ view: UIView?) -> CGRect { if let superview = superview { return superview.convert(frame, to: view) } return frame } } extension UIView { /// The method drawViewHierarchyInRect:afterScreenUpdates: performs its operations on the GPU as much as possible /// In comparison, the method renderInContext: performs its operations inside of your app's address space and does /// not use the GPU based process for performing the work. /// https://stackoverflow.com/a/25704861/1418981 public func caSnapshot(scale: CGFloat = 0, isOpaque: Bool = false) -> UIImage? { var isSuccess = false UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, scale) if let context = UIGraphicsGetCurrentContext() { layer.render(in: context) isSuccess = true } let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return isSuccess ? image : nil } } 

Resultado (animación gif):

Animación Gif