// // FullScreenTransitionManager.swift // import Foundation import UIKit // MARK: FullScreenPresentationController final class FullScreenPresentationController: UIPresentationController { private let backgroundView: UIView = { let view = UIView() view.backgroundColor = .systemBackground view.alpha = 0 return view }() private lazy var tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onTap)) @objc private func onTap(_ gesture: UITapGestureRecognizer) { presentedViewController.dismiss(animated: true) } } // MARK: UIPresentationController extension FullScreenPresentationController { override func presentationTransitionWillBegin() { guard let containerView = containerView else { return } containerView.addGestureRecognizer(tapGestureRecognizer) containerView.addSubview(backgroundView) backgroundView.frame = containerView.frame guard let transitionCoordinator = presentingViewController.transitionCoordinator else { return } transitionCoordinator.animate(alongsideTransition: { context in self.backgroundView.alpha = 1 }) } override func presentationTransitionDidEnd(_ completed: Bool) { if !completed { backgroundView.removeFromSuperview() containerView?.removeGestureRecognizer(tapGestureRecognizer) } } override func dismissalTransitionWillBegin() { guard let transitionCoordinator = presentingViewController.transitionCoordinator else { return } transitionCoordinator.animate(alongsideTransition: { context in self.backgroundView.alpha = 0 }) } override func dismissalTransitionDidEnd(_ completed: Bool) { if completed { backgroundView.removeFromSuperview() containerView?.removeGestureRecognizer(tapGestureRecognizer) } } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { guard let containerView = containerView else { return } coordinator.animate(alongsideTransition: { context in self.backgroundView.frame = containerView.frame }) } } // MARK: FullScreenTransitionManager final class FullScreenTransitionManager: NSObject, UIViewControllerTransitioningDelegate { fileprivate enum AnimationState { case present case dismiss } private weak var anchorView: UIView? private var animationState: AnimationState = .present private var animationDuration: TimeInterval = Resources.animation.duration private var anchorViewFrame: CGRect = .zero private var propertyAnimator: UIViewPropertyAnimator? init(anchorView: UIView) { self.anchorView = anchorView } func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { FullScreenPresentationController(presentedViewController: presented, presenting: presenting) } func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { prepare(animationState: .present) } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { // Starting the animation here, since UIKit do not update safe area insets after UIViewController.viewWillDisappear() is called defer { propertyAnimator = dismissAnimator(animating: dismissed) } return prepare(animationState: .dismiss) } } // MARK: UIViewControllerAnimatedTransitioning extension FullScreenTransitionManager: UIViewControllerAnimatedTransitioning { private func prepare(animationState: AnimationState, animationDuration: TimeInterval = Resources.animation.duration) -> UIViewControllerAnimatedTransitioning? { guard let anchorView = anchorView else { return nil } self.animationState = animationState self.animationDuration = animationDuration self.anchorViewFrame = anchorView.safeAreaLayoutGuide.layoutFrame return self } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { animationDuration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { switch animationState { case .present: guard let toViewController = transitionContext.viewController(forKey: .to) else { return transitionContext.completeTransition(false) } propertyAnimator = presentAnimator(with: transitionContext, animating: toViewController) case .dismiss: guard let fromViewController = transitionContext.viewController(forKey: .from) else { return transitionContext.completeTransition(false) } propertyAnimator = updatedDismissAnimator(with: transitionContext, animating: fromViewController) } } private func presentAnimator(with transitionContext: UIViewControllerContextTransitioning, animating viewController: UIViewController) -> UIViewPropertyAnimator { transitionContext.containerView.addSubview(viewController.view) let finalFrame = transitionContext.finalFrame(for: viewController) viewController.view.frame = anchorViewFrame viewController.view.layoutIfNeeded() return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut], animations: { viewController.view.frame = finalFrame viewController.view.layoutIfNeeded() }, completion: { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) }) } private func dismissAnimator(animating viewController: UIViewController) -> UIViewPropertyAnimator { let finalFrame = anchorViewFrame return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: animationDuration, delay: 0, options: [.curveEaseInOut], animations: { viewController.view.frame = finalFrame viewController.view.layoutIfNeeded() }) } private func updatedDismissAnimator(with transitionContext: UIViewControllerContextTransitioning, animating viewController: UIViewController) -> UIViewPropertyAnimator { let propertyAnimator = self.propertyAnimator ?? dismissAnimator(animating: viewController) propertyAnimator.addCompletion({ _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) }) self.propertyAnimator = propertyAnimator return propertyAnimator } }