Skip to content
Snippets Groups Projects
CenterTransition.swift 4.89 KiB
Newer Older
Bruno Muniz's avatar
Bruno Muniz committed
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()
            })
    }
}