import UIKit import Combine import SnapKit import Shared final class CenterTransition: NSObject, UIViewControllerAnimatedTransitioning { enum Direction { case present case dismiss } let dismissable: Bool var direction: Direction = .present private let onDismissal: EmptyClosure private weak var darkOverlayView: UIControl? private weak var topConstraint: Constraint? private weak var bottomConstraint: Constraint? private var cancellables = Set<AnyCancellable>() private var presentedConstraints: [NSLayoutConstraint] = [] private var dismissedConstraints: [NSLayoutConstraint] = [] init(onDismissal: @escaping EmptyClosure, dismissable: Bool = true) { self.dismissable = dismissable self.onDismissal = onDismissal super.init() } func transitionDuration(using context: UIViewControllerContextTransitioning?) -> TimeInterval { 0.25 } func animateTransition(using context: UIViewControllerContextTransitioning) { switch direction { case .present: present(using: context) case .dismiss: dismiss(using: context) } } private func present(using context: UIViewControllerContextTransitioning) { guard let presentingController = context.viewController(forKey: .from), let presentedView = context.view(forKey: .to) else { context.completeTransition(false) return } let darkOverlayView = UIControl() self.darkOverlayView = darkOverlayView darkOverlayView.alpha = 0.0 darkOverlayView.backgroundColor = UIColor.black.withAlphaComponent(0.5) context.containerView.addSubview(darkOverlayView) darkOverlayView.frame = context.containerView.bounds if dismissable { darkOverlayView .publisher(for: .touchUpInside) .sink { [weak presentingController] _ in presentingController?.dismiss(animated: true) } .store(in: &cancellables) } context.containerView.addSubview(presentedView) presentedView.translatesAutoresizingMaskIntoConstraints = false presentedView.alpha = 0.0 presentedView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) presentedConstraints = [ presentedView.leftAnchor.constraint(equalTo: context.containerView.leftAnchor, constant: 40), presentedView.rightAnchor.constraint(equalTo: context.containerView.rightAnchor, constant: -40), presentedView.centerYAnchor.constraint(equalTo: context.containerView.centerYAnchor) ] dismissedConstraints = [ presentedView.leftAnchor.constraint(equalTo: context.containerView.leftAnchor, constant: 40), presentedView.rightAnchor.constraint(equalTo: context.containerView.rightAnchor, constant: -40), presentedView.centerYAnchor.constraint(equalTo: context.containerView.centerYAnchor) ] NSLayoutConstraint.activate(dismissedConstraints) context.containerView.setNeedsLayout() context.containerView.layoutIfNeeded() NSLayoutConstraint.deactivate(dismissedConstraints) NSLayoutConstraint.activate(presentedConstraints) UIView.animate( withDuration: transitionDuration(using: context), delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseInOut, animations: { darkOverlayView.alpha = 1.0 presentedView.alpha = 1.0 context.containerView.setNeedsLayout() context.containerView.layoutIfNeeded() presentedView.transform = .identity }, completion: { _ in context.completeTransition(true) }) } private func dismiss(using context: UIViewControllerContextTransitioning) { NSLayoutConstraint.deactivate(presentedConstraints) NSLayoutConstraint.activate(dismissedConstraints) guard let presentedView = context.view(forKey: .from) else { context.completeTransition(false) return } UIView.animate( withDuration: transitionDuration(using: context), delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseInOut, animations: { [weak darkOverlayView] in darkOverlayView?.alpha = 0.0 presentedView.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) presentedView.alpha = 0.0 context.containerView.setNeedsLayout() context.containerView.layoutIfNeeded() }, completion: { [weak self] _ in context.completeTransition(true) self?.onDismissal() }) } }