Sharing an image between two viewControllers during a transition animation

I came across really steep transitions between viewControllers, because the UIViewControllerAnimatedTransitioning protocol was available in IOS 7. I recently noticed something particularly interesting in the Intacart IOS application.

Here is the animation I'm talking about in slow motion: https://www.dropbox.com/s/p2hxj45ycq18i3l/Video%20Oct%2015%2C%207%2023%2059%20PM.mov?dl=0

At first, I thought it looked like what the author is viewing in this tutorial, with some additional fade-in and fade-out animations: http://www.raywenderlich.com/113845/ios-animation-tutorial-custom-view -controller-presentation-transitions

But then, if you look at it carefully, it looks like the product images are moving between the two viewControllers as the first viewController disappears. The reason I think there are two viewControllers is because when you scroll down a new view, you can still see the original view behind it without changing the layout.

Perhaps the two viewControllers really have a product image (not faded) and somehow come to life at the same time with perfect accuracy, and one of them disappears when the other disappears.

What do you think is going on there?

How can I program a transition animation that looks like the image is divided between two viewControllers?

+4
source share
2

, . , .

. , :

enter image description here

, , . , . , , , , .

( , ): , , :

enter image description here

+3

(Swift 4):

:

  • InterViewAnimatable. .
  • . .
  • .
  • .

, , .

: 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 (i.e. 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
   }
}

:

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

:

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

( gif):

Gif animation

0

Source: https://habr.com/ru/post/1611865/


All Articles