// // 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 } self.presentedViewController.view.backgroundColor = .systemRed 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, let presentedView = presentedView else { return } coordinator.animate(alongsideTransition: { context in self.backgroundView.frame = containerView.frame presentedView.frame = self.frameOfPresentedViewInContainerView }) } } // MARK: FullScreenTransitionManager final class FullScreenTransitionManager: NSObject, UIViewControllerTransitioningDelegate { private weak var anchorView: UIView? 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? { let anchorFrame = anchorView?.safeAreaLayoutGuide.layoutFrame ?? CGRect(origin: presented.view.center, size: .zero) return FullScreenAnimationController(animationType: .present, anchorFrame: anchorFrame) } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { let anchorFrame = anchorView?.safeAreaLayoutGuide.layoutFrame ?? CGRect(origin: dismissed.view.center, size: .zero) return FullScreenAnimationController(animationType: .dismiss, anchorFrame: anchorFrame) } } // MARK: UIViewControllerAnimatedTransitioning final class FullScreenAnimationController: NSObject, UIViewControllerAnimatedTransitioning { enum AnimationType { case present case dismiss } private let animationType: AnimationType private let anchorFrame: CGRect private let animationDuration: TimeInterval private var propertyAnimator: UIViewPropertyAnimator? init(animationType: AnimationType, anchorFrame: CGRect, animationDuration: TimeInterval = 0.3) { self.animationType = animationType self.anchorFrame = anchorFrame self.animationDuration = animationDuration } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { animationDuration } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { switch animationType { case .present: guard let toViewController = transitionContext.viewController(forKey: .to) else { return transitionContext.completeTransition(false) } transitionContext.containerView.addSubview(toViewController.view) propertyAnimator = presentAnimator(with: transitionContext, animating: toViewController) case .dismiss: guard let fromViewController = transitionContext.viewController(forKey: .from) else { return transitionContext.completeTransition(false) } propertyAnimator = dismissAnimator(with: transitionContext, animating: fromViewController) } } private func presentAnimator(with transitionContext: UIViewControllerContextTransitioning, animating viewController: UIViewController) -> UIViewPropertyAnimator { let finalFrame = transitionContext.finalFrame(for: viewController) let safeAreaInsets = transitionContext.containerView.safeAreaInsets let safeAreaCompensation = UIEdgeInsets(top: -safeAreaInsets.top, left: -safeAreaInsets.left, bottom: -safeAreaInsets.bottom, right: -safeAreaInsets.right) viewController.additionalSafeAreaInsets = safeAreaCompensation viewController.view.frame = anchorFrame viewController.view.setNeedsLayout() viewController.view.layoutIfNeeded() return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut, .layoutSubviews], animations: { viewController.additionalSafeAreaInsets = .zero viewController.view.frame = finalFrame viewController.view.setNeedsLayout() viewController.view.layoutIfNeeded() }, completion: { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) }) } private func dismissAnimator(with transitionContext: UIViewControllerContextTransitioning, animating viewController: UIViewController) -> UIViewPropertyAnimator { let finalFrame = anchorFrame let safeAreaInsets = transitionContext.containerView.safeAreaInsets let safeAreaCompensation = UIEdgeInsets(top: -safeAreaInsets.top, left: -safeAreaInsets.left, bottom: -safeAreaInsets.bottom, right: -safeAreaInsets.right) viewController.view.setNeedsLayout() viewController.view.layoutIfNeeded() return UIViewPropertyAnimator.runningPropertyAnimator(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.curveEaseInOut, .layoutSubviews], animations: { viewController.additionalSafeAreaInsets = safeAreaCompensation viewController.view.frame = finalFrame viewController.view.setNeedsLayout() viewController.view.layoutIfNeeded() }, completion: { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) }) } }