// // LargeImageExpandAnimationController.swift // Tusker // // Created by Shadowfacts on 9/1/18. // Copyright © 2018 Shadowfacts. All rights reserved. // import UIKit protocol LargeImageAnimatableViewController: UIViewController { var animationSourceView: UIImageView? { get } var largeImageController: LargeImageViewController? { get } var animationImage: UIImage? { get } var animationGifData: Data? { get } var dismissInteractionController: LargeImageInteractionController? { get } var isInteractivelyAnimatingDismissal: Bool { get set } } extension LargeImageAnimatableViewController { func sourceViewFrame(in coordinateSpace: UIView) -> CGRect? { guard let sourceView = animationSourceView else { return nil } var sourceFrame = sourceView.convert(sourceView.bounds, to: coordinateSpace) if let scrollView = coordinateSpace as? UIScrollView { let scale = scrollView.zoomScale let width = sourceFrame.width * scale let height = sourceFrame.height * scale let x = sourceFrame.minX * scale - scrollView.contentOffset.x + scrollView.frame.minX let y = sourceFrame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY sourceFrame = CGRect(x: x, y: y, width: width, height: height) } return sourceFrame } } class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { if UIAccessibility.prefersCrossFadeTransitions { return 0.2 } else { return 0.4 } } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let fromVC = transitionContext.viewController(forKey: .from), let toVC = transitionContext.viewController(forKey: .to) as? LargeImageAnimatableViewController else { return } if UIAccessibility.prefersCrossFadeTransitions { animateCrossFadeTransition(using: transitionContext) return } let containerView = transitionContext.containerView containerView.addSubview(toVC.view) let finalVCFrame = transitionContext.finalFrame(for: toVC) guard let sourceView = toVC.animationSourceView, let sourceFrame = toVC.sourceViewFrame(in: fromVC.view), let image = toVC.animationImage else { toVC.view.frame = finalVCFrame transitionContext.completeTransition(!transitionContext.transitionWasCancelled) return } // use alpha, because isHidden makes stack views re-layout sourceView.alpha = 0 toVC.view.alpha = 0 toVC.largeImageController?.contentView.isHidden = true toVC.largeImageController?.setControlsVisible(false, animated: false) var finalFrameSize = finalVCFrame.inset(by: toVC.view.safeAreaInsets).size let newWidth = finalFrameSize.width / image.size.width let newHeight = finalFrameSize.height / image.size.height if newHeight < newWidth { finalFrameSize.width = newHeight * image.size.width } else { finalFrameSize.height = newWidth * image.size.height } let finalFrame = CGRect(origin: CGPoint(x: finalVCFrame.midX - finalFrameSize.width / 2, y: finalVCFrame.midY - finalFrameSize.height / 2), size: finalFrameSize) let imageView = GIFImageView(frame: sourceFrame) imageView.image = image if let gifData = toVC.animationGifData { imageView.animate(withGIFData: gifData) } imageView.contentMode = .scaleAspectFill imageView.layer.cornerRadius = sourceView.layer.cornerRadius imageView.layer.maskedCorners = sourceView.layer.maskedCorners imageView.layer.masksToBounds = true containerView.addSubview(imageView) let duration = transitionDuration(using: transitionContext) let velocity = 1 / CGFloat(duration) UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.75, initialSpringVelocity: velocity, options: []) { imageView.frame = finalFrame imageView.layer.cornerRadius = 0 toVC.view.alpha = 1 toVC.largeImageController?.setControlsVisible(true, animated: false) } completion: { (_) in // This shouldn't be necessary. I believe it's a workaround for using a XIB // for the large image VC. Without this, the final frame of the large image VC // is not set to the propper rect (it uses the frame of the preview device // in the XIB). When using a storyboard, the final frame is automatically set // (or UIKit does layout differently when loading the view) and this is not necessary. toVC.view.frame = finalVCFrame toVC.largeImageController?.contentView.isHidden = false fromVC.view.isHidden = false imageView.removeFromSuperview() sourceView.alpha = 1 transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } } func animateCrossFadeTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let toVC = transitionContext.viewController(forKey: .to) as? LargeImageAnimatableViewController else { return } transitionContext.containerView.addSubview(toVC.view) toVC.view.alpha = 0 let duration = transitionDuration(using: transitionContext) UIView.animate(withDuration: duration) { toVC.view.alpha = 1 } completion: { (_) in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } } }