이전 글에서 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의 연장선에서 알아보겠다. 이전에 CardDetailViewControllerUIViewControllerTransitioningDelegate를 채택하고 여기서 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가 여기에 담겨있다. 사실 이 부분은 나중에 구현하면서 삽질할 것이기 때문에 덜 중요하다고 판단하여 스킵한다.

마무리

이렇게 Custom Transition의 개념을 알았다! interact가 필요한 경우에는 다른 분의 youtube링크를 포함해둔다. 혹시라도 모르면 그 때가서 보고 따라하는 것으로 해결해보자. 끝!

Reference

profile
Goal, Plan, Execute.

0개의 댓글