// // GalleryPresentationAnimationController.swift // GalleryVC // // Created by Shadowfacts on 12/28/23. // import UIKit class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimatedTransitioning { private let sourceView: UIView init(sourceView: UIView) { self.sourceView = sourceView } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.4 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let to = transitionContext.viewController(forKey: .to) as? GalleryViewController else { fatalError() } let itemViewController = to.currentItemViewController if !itemViewController.content.canAnimateFromSourceView || UIAccessibility.prefersCrossFadeTransitions { animateCrossFadeTransition(using: transitionContext) return } let container = transitionContext.containerView to.view.frame = container.bounds container.addSubview(to.view) container.layoutIfNeeded() // Make sure the zoom scale is updated before getting the content view frame, since it needs to take into account the correct transform. itemViewController.updateZoomScale(resetZoom: true) let sourceFrameInContainer = container.convert(sourceView.bounds, from: sourceView) let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view) // Use a transformation to make the actual source view appear to move into the destination frame. // Doing this while having the content view fade-in papers over the z-index change when // there was something overlapping the source view. let origSourceTransform = sourceView.transform let sourceToDestTransform: CGAffineTransform? if destFrameInContainer.width > 0 && destFrameInContainer.height > 0 { // Scale evenly in both dimensions, to prevent the source view appearing to stretch/distort during the animation. let scale = min(destFrameInContainer.width / sourceFrameInContainer.width, destFrameInContainer.height / sourceFrameInContainer.height) sourceToDestTransform = origSourceTransform .translatedBy(x: destFrameInContainer.midX - sourceFrameInContainer.midX, y: destFrameInContainer.midY - sourceFrameInContainer.midY) .scaledBy(x: scale, y: scale) } else { sourceToDestTransform = nil } let content = itemViewController.takeContent() content.view.translatesAutoresizingMaskIntoConstraints = true container.insertSubview(content.view, belowSubview: to.view) // Use a separate dimming view from to.view, so that the gallery controls can be in front of the moving content. let dimmingView = UIView() dimmingView.backgroundColor = .black dimmingView.frame = container.bounds dimmingView.layer.opacity = 0 container.insertSubview(dimmingView, belowSubview: content.view) to.view.backgroundColor = nil to.view.layer.opacity = 0 content.view.frame = sourceFrameInContainer content.view.layer.opacity = 0 container.layoutIfNeeded() // This needs to take place after the layout, so that the transform is correct. itemViewController.setControlsVisible(false, animated: false) let duration = self.transitionDuration(using: transitionContext) // rougly equivalent to duration: 0.35, bounce: 0.3 let spring = UISpringTimingParameters(mass: 1, stiffness: 322, damping: 25, initialVelocity: .zero) let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring) animator.addAnimations { dimmingView.layer.opacity = 1 to.view.layer.opacity = 1 content.view.frame = destFrameInContainer content.view.layer.opacity = 1 itemViewController.setControlsVisible(true, animated: false) if let sourceToDestTransform { self.sourceView.transform = sourceToDestTransform } } animator.addCompletion { _ in dimmingView.removeFromSuperview() to.view.backgroundColor = .black if sourceToDestTransform != nil { self.sourceView.transform = origSourceTransform } itemViewController.addContent() transitionContext.completeTransition(true) } animator.startAnimation() } private func animateCrossFadeTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let to = transitionContext.viewController(forKey: .to) as? GalleryViewController else { return } to.view.alpha = 0 to.view.frame = transitionContext.containerView.bounds transitionContext.containerView.addSubview(to.view) let duration = transitionDuration(using: transitionContext) let animator = UIViewPropertyAnimator(duration: duration, curve: .easeInOut) animator.addAnimations { to.view.alpha = 1 } animator.addCompletion { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) } animator.startAnimation() } }