์ด์ „ ๊ธ€์—์„œ PresentationController๊ฐ€ ๋ฌด์—‡์ธ์ง€ ๋ฐฐ์› ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ, Transition Animator๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋ถ€๋ถ„์—์„œ UIViewControllerAnimatedTransitioning์ด๋ผ๋Š” ์นœ๊ตฌ๋ฅผ ๋ณด์•˜๋‹ค. ์˜ค๋Š˜์˜ ์ฃผ์ œ๋Š” ์ด๋…€์„์ด๋‹ค.

UIViewControllerAnimatedTransitioning

Custom Transition์— ์žˆ์–ด ํ•„์š”ํ•œ animation์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•œ method๋“ค์ด ๋ชจ์—ฌ ์žˆ๋Š” ํ”„๋กœํ† ์ฝœ์ด๋‹ค. ์ด ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•œ ๊ตฌํ˜„์ฒด๋ฅผ ๋„˜๊ฒจ์ฃผ๋ฉด ๋œ๋‹ค.

ํ•ด๋‹น Protocol์˜ method๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, โ€œ๊ณ ์ •๋œ ์‹œ๊ฐ„ ๋‚ด์— VC๋ฅผ ํ™”๋ฉด๋ฐ–์œผ๋กœ ์ „ํ™˜ํ•˜๊ธฐ ์œ„ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ •์˜ํ•œ ๊ฐ์ฒดโ€๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. ๋Œ€ํ™”ํ˜• ๊ฐ™์€ ๊ฒฝ์šฐ UIViewControllerInteractiveTransitioning๋ฅผ ํ†ตํ•ด ์ฒ˜๋ฆฌํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค. ๊ตฌํ˜„ํ•ด์•ผ ํ•˜๋Š” method๋“ค์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ฒƒ๋“ค์ด ์žˆ๋‹ค.

  • transitionDuration(using:): Required
    • Transition Animation์˜ Duration์„ ์ง€์ •ํ•œ๋‹ค.
  • animateTransition(using:): Required
    • animation์„ ์ •์˜ํ•ด์ค€๋‹ค.
    • ์ƒํ™ฉ์— ๋งž๋Š” ์—ฌ๋Ÿฌ ๋‹ค๋ฅธ animator๋ฅผ ์ œ๊ณตํ•  ์ˆ˜๋„ ์žˆ๋‹ค. (์˜ˆ๋ฅผ ๋“ค์–ด .present, .dismiss)

๋‘ Method ๋ชจ๋‘ UIViewControllerContextTransitioning์ด๋ผ๋Š” Protocol ๊ตฌํ˜„์ฒด๋กœ๋ถ€ํ„ฐ Transition์ด ์ผ์–ด๋‚˜๋Š” ๋™์•ˆ์˜ ์ •๋ณด๋“ค์„ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ Transition Context๋ผ ํ•œ๋‹ค. ๊ทธ ์•ˆ์—๋Š” ์ด์ „ ๊ธ€์—์„œ ์„ค๋ช…ํ•œ containerView, Frame ์ •๋ณด, isInteractive, isAnimated ๋“ฑ๋“ฑ์˜ ์ •๋ณด๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.

Project

์ด์ „์— ๋ณด์•˜๋˜ AppStore์˜ ์—ฐ์žฅ์„ ์—์„œ ์•Œ์•„๋ณด๊ฒ ๋‹ค. ์ด์ „์— CardDetailViewController๊ฐ€ UIViewControllerTransitioningDelegate๋ฅผ ์ฑ„ํƒํ•˜๊ณ  ์—ฌ๊ธฐ์„œ 3๊ฐœ์˜ method๋ฅผ ๊ตฌํ˜„ํ•ด์คฌ์—ˆ๋Š”๋ฐ, Presentation Controller์™€ ๊ด€๊ณ„ ์—†๋Š” ๋ฉ”์„œ๋“œ๊ฐ€ ๋‘˜ ์žˆ์—ˆ๋‹ค.

extension CardDetailViewController: UIViewControllerTransitioningDelegate {
    
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return TodayAnimationTransition(animationType: .present)
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return TodayAnimationTransition(animationType: .dismiss)
    }
    
    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        return CardPresentationController(presentedViewController: presented, presenting: presenting)
    }
}

์œ„์˜ ๋‘ ๋ฉ”์„œ๋“œ๊ฐ€ ๊ทธ๊ฒƒ์ด๋‹ค. ์ด์ „ ๊ธ€์—์„œ๋Š” present, dismiss์— ๋งž๋Š” ๊ฐ๊ฐ์˜ animator๋ฅผ ์ œ๊ณตํ•ด์ฃผ์—ˆ๋‹ค๊ณ  ํ–ˆ์—ˆ๋‹ค. ์ด์ œ๋Š” ์ €๋…€์„์˜ ์‹ค์ฒด๋ฅผ ํ™•์ธํ•  ์ฐจ๋ก€์ด๋‹ค.

TodayAnimationTransition

fileprivate let transitonDuration: TimeInterval = 1.0
 
enum AnimationType {
    case present
    case dismiss
}
 
class TodayAnimationTransition: NSObject {
    let animationType: AnimationType!
    
    init(animationType: AnimationType) {
        self.animationType = animationType
        super.init()
    }
}

์ผ๋‹จ ๊ธฐ๋ณธ ๋ฐ˜์ฐฌ๋ถ€ํ„ฐ ๋ณด์ž. animator์— ๊ด€๋ จ๋œ ๊ฒƒ์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด์„œ time interval์„ ์ •์˜ํ•˜์˜€๊ณ , Type๊นŒ์ง€ ๋‚˜๋ˆ ์„œ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ๋‹ค. ์ดˆ๊ธฐํ™”ํ•  ๋•Œ, Type์„ ์ •์˜ํ•˜๋ฉด, ์ด์— ๋งž๋Š” Animator๋ฅผ ์ œ๊ณตํ•  ์ƒ๊ฐ์ธ๊ฐ€๋ณด๋‹ค.

extension TodayAnimationTransition: UIViewControllerAnimatedTransitioning {
 
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return transitonDuration
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        if animationType == .present {
            animationForPresent(using: transitionContext)
        } else {
            animationForDismiss(using: transitionContext)
        }
    }
 
}

์•„๊นŒ ๋งํ–ˆ๋˜ UIViewControllerAnimatedTransitioning๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ Required๋˜๋Š” ๋‘๊ฐœ์˜ method๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ๋‹ค. animation ์ง€์† ์‹œ๊ฐ„๊ณผ, ์–ด๋–ค animation์„ ์ง„ํ–‰ํ•  ๊ฒƒ์ธ์ง€์— ๋Œ€ํ•œ method์ด๋‹ค. ์ฝ”๋“œ ์ž‘์„ฑ์ž๋Š” type์— ๋งž๊ฒŒ ๋‘๊ฐœ์˜ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ณ  ์žˆ๋‹ค.

extension TodayAnimationTransition: UIViewControllerAnimatedTransitioning {
 
    func animationForPresent(using transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView
        //1.Get fromVC and toVC
        guard let fromVC = transitionContext.viewController(forKey: .from) as? UITabBarController else { return }
        guard let tableViewController = fromVC.viewControllers?.first as? TodayViewController else { return }
        guard let toVC = transitionContext.viewController(forKey: .to) as? CardDetailViewController else { return }
        guard let selectedCell = tableViewController.selectedCell else { return }
        
        let frame = selectedCell.convert(selectedCell.bgBackView.frame, to: fromVC.view)        
        //2.Set presentation original size.
        toVC.view.frame = frame
        toVC.scrollView.imageView.frame.size.width = GlobalConstants.todayCardSize.width
        toVC.scrollView.imageView.frame.size.height = GlobalConstants.todayCardSize.height
        
        containerView.addSubview(toVC.view)
        
        //3.Change original size to final size with animation.
        UIView.animate(withDuration: transitonDuration, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0, options: [], animations: {
            toVC.view.frame = UIScreen.main.bounds
            toVC.scrollView.imageView.frame.size.width = kScreenW
            toVC.scrollView.imageView.frame.size.height = GlobalConstants.cardDetailTopImageH
            toVC.closeBtn.alpha = 1
            
            fromVC.tabBar.frame.origin.y = kScreenH
        }) { (completed) in
            transitionContext.completeTransition(completed)
        }
    }
    
    func animationForDismiss(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromVC = transitionContext.viewController(forKey: .from) as? CardDetailViewController else { return }
        guard let toVC = transitionContext.viewController(forKey: .to) as? UITabBarController else { return }
        guard let tableViewController = toVC.viewControllers?.first as? TodayViewController else { return }
        guard let selectedCell = tableViewController.selectedCell else { return }
        
        UIView.animate(withDuration: transitonDuration - 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0, options: [], animations: {
            let frame = selectedCell.convert(selectedCell.bgBackView.frame, to: toVC.view)
            fromVC.view.frame = frame
            fromVC.view.layer.cornerRadius = GlobalConstants.toDayCardCornerRadius
            fromVC.scrollView.imageView.frame.size.width = GlobalConstants.todayCardSize.width
            fromVC.scrollView.imageView.frame.size.height = GlobalConstants.todayCardSize.height
            fromVC.closeBtn.alpha = 0
            
            toVC.tabBar.frame.origin.y = kScreenH - toVC.tabBar.frame.height
        }) { (completed) in
            transitionContext.completeTransition(completed)
            toVC.view.addSubview(toVC.tabBar)
        }
    }
    
}

์‹ค์ œ ์ง„ํ–‰ํ•˜๋Š” code๊ฐ€ ์—ฌ๊ธฐ์— ๋‹ด๊ฒจ์žˆ๋‹ค. ์‚ฌ์‹ค ์ด ๋ถ€๋ถ„์€ ๋‚˜์ค‘์— ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์‚ฝ์งˆํ•  ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋œ ์ค‘์š”ํ•˜๋‹ค๊ณ  ํŒ๋‹จํ•˜์—ฌ ์Šคํ‚ตํ•œ๋‹ค.

Reference