// // GalleryPresentationAnimationController.swift // GalleryVC // // Created by Shadowfacts on 12/28/23. // import UIKit class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimatedTransitioning { private let sourceView: UIView private var completionHandlers: [() -> Void] = [] init(sourceView: UIView) { self.sourceView = sourceView } func addCompletionHandler(_ block: @escaping () -> Void) { completionHandlers.append(block) } 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() } to.presentationAnimationController = self let itemViewController = to.currentItemViewController if !itemViewController.content.canAnimateFromSourceView || UIAccessibility.prefersCrossFadeTransitions { animateCrossFadeTransition(using: transitionContext) return } let container = transitionContext.containerView itemViewController.view.layoutIfNeeded() 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 { sourceToDestTransform = origSourceTransform .translatedBy(x: destFrameInContainer.midX - sourceFrameInContainer.midX, y: destFrameInContainer.midY - sourceFrameInContainer.midY) .scaledBy(x: destFrameInContainer.width / sourceFrameInContainer.width, y: destFrameInContainer.height / sourceFrameInContainer.height) } else { sourceToDestTransform = nil } let content = itemViewController.takeContent() content.view.translatesAutoresizingMaskIntoConstraints = true container.addSubview(to.view) container.addSubview(content.view) 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.4, bounce: 0.3 let spring = UISpringTimingParameters(mass: 1, stiffness: 247, damping: 22, initialVelocity: .zero) let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring) animator.addAnimations { 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 if sourceToDestTransform != nil { self.sourceView.transform = origSourceTransform } itemViewController.addContent() transitionContext.completeTransition(true) for block in self.completionHandlers { block() } to.presentationAnimationController = nil } animator.startAnimation() } private func animateCrossFadeTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let to = transitionContext.viewController(forKey: .to) as? GalleryViewController else { return } transitionContext.containerView.addSubview(to.view) to.view.alpha = 0 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) for block in self.completionHandlers { block() } to.presentationAnimationController = nil } animator.startAnimation() } }