티스토리 뷰

안녕하세요 Diana 입니다.

 

오늘은 Transitioning Delegate를 사용하여 화면전환 애니메이션을 직접 구현해보도록 하겠습니다.

 


 

시작하기에 앞서 제가 구현하고자 하는 애니메이션은 아래와 같습니다.

 

저는 애니메이션을 구현하기 위해 UIViewTransitioningDelegate를 만족하는 CustomTransition 클래스를 따로 구현하여 필요할때마다 가져와 사용할 수 있도록 하였습니다.

CustomTransition 코드에 앞서 아래 코드는 Cell이 선택되었을 때 CustomTransition 객체를 만들어 이동하고자 하는 DetailContentViewController에 적용해주고 있는 내용입니다.

 

ReferenceView는 애니메이션의 시작점이 되는 뷰이며 해당 좌표에서 시작하여 뷰는 이동하게 됩니다.

그리고 detailVC의 transitioningDelegate에 우리가 구현한 CustomTransition을 적용시켜 UIKit에 우리는 커스텀한 화면전환을 사용할 것이다 라는 것을 전달해주었습니다. 

 

(TransitionConfiguration은 애니메이션에 사용되는 duration, delay 값 등을 담고 있는 구조체 입니다.)

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        guard let cell = collectionView.cellForItem(at: indexPath) else { return }
        let albumInfo = albumInfos[indexPath.item]
        
        let detailVC = DetailContentViewController(viewModel: .init(albumInfo: albumInfo))
        detailVC.modalPresentationStyle = .custom
        
        let referenceView = cell.contentView
        
        let config = TransitionConfiguration()
        let transition = CustomTransition(config: config, referenceView: referenceView)
        detailVC.transitioningDelegate = transition
        
        present(detailVC, animated: true, completion: nil)
    }

 

이제 CustomTransition 코드를 살펴보겠습니다.

 

import UIKit

final class CustomTransition: NSObject {
    private let config: TransitionConfiguration
    private let referenceView: UIView
    
    private let presentingTransitionAnimator: PresentingTransitionAnimator
    private let dismissTransitionAnimator: DismissTransitionAnimator
    
    private var presentationController: UIPresentationController?
    private var currentTranslationY: CGFloat = 0
    
    init(config: TransitionConfiguration = TransitionConfiguration(), referenceView: UIView) {
        self.config = config
        self.referenceView = referenceView
        
        presentingTransitionAnimator = PresentingTransitionAnimator(
            config: config,
            referenceView: referenceView
        )
        
        dismissTransitionAnimator = DismissTransitionAnimator(
            config: config,
            referenceView: referenceView
        )
    }
}

extension CustomTransition: UIViewControllerTransitioningDelegate {
    // 뷰 컨트롤러를 프레젠팅할 때 트랜지션 애니메이터 객체를 요청하는 메서드
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> (any UIViewControllerAnimatedTransitioning)? {
        return presentingTransitionAnimator
    }
    
    // 뷰 컨트롤러를 디스미스할 때 트렌지션 애니메이터 객체를 요청하는 메서드
    func animationController(forDismissed dismissed: UIViewController) -> (any UIViewControllerAnimatedTransitioning)? {
        return dismissTransitionAnimator
    }
    
    // Interactive animator 객체가 있는지 확인(Present)
    func interactionControllerForPresentation(using animator: any UIViewControllerAnimatedTransitioning) -> (any UIViewControllerInteractiveTransitioning)? {
        return nil
    }
    
    // Interactive animator 객체가 있는지 확인(Dismiss)
    func interactionControllerForDismissal(using animator: any UIViewControllerAnimatedTransitioning) -> (any UIViewControllerInteractiveTransitioning)? {
        return nil
    }
    
	...
    
    @objc
    private func handlePanGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
		...
    }
}

 

CustomTraisition 코드를 보면 referenceView와 Present 할때와 Dismiss 할때의 Animator 그리고 PresentationController를 가지고 있습니다.

 

여기서 referenceView는 앞에서 설명했듯이 애니메이션의 시작점이 되는 View이며 위 그림의 빨간 Cell을 나타냅니다.

 

각각의 애니메이션에 관련된 상세 코드는 Animator 내부를 살펴보아야 합니다.

 

우리는 CustomTraisition의 animationController 함수를 통해 각각의 Animator 객체를 return 해줌으로써 해당 객체들을 사용하겠다는 것을 설정할 수 있습니다.

 

import UIKit

final class PresentingTransitionAnimator: NSObject {
    private let config: TransitionConfiguration
    private let referenceView: UIView
    private var transitionContext: UIViewControllerContextTransitioning? // 애니메이션 실행시 필요한 정보
    
    init(config: TransitionConfiguration, referenceView: UIView) {
        self.config = config
        self.referenceView = referenceView
    }
}

extension PresentingTransitionAnimator: UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: (any UIViewControllerContextTransitioning)?) -> TimeInterval {
        return config.duration
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let container = transitionContext.containerView
        
        guard let toVC = transitionContext.viewController(forKey: .to),
              let detailVC = toVC as? DetailContentViewController,
              let targetImageViewFrame = detailVC.getContentFrame() else {
            transitionContext.completeTransition(false)
            return
        }
        
        let startFrame = referenceView.convert(referenceView.bounds, to: container)
        
        let finalFrame = transitionContext.finalFrame(for: toVC)
        toVC.view.frame = finalFrame
        container.addSubview(toVC.view)

        var endFrame: CGRect = container.bounds
        
        let thumbnailImageView = UIImageView() // 추가
        var targetImageView: UIView = UIView()
        
        if let detailVC = toVC as? DetailContentViewController{
            print("TargetImageView = \(targetImageView.frame)")
            targetImageView = detailVC.getDetailContentView() ?? UIView()
            
            thumbnailImageView.image = detailVC.getThumbnailView()
            thumbnailImageView.frame = startFrame
            targetImageView.alpha = 0
            endFrame = targetImageView.convert(targetImageViewFrame, to: container)
        }
        
        toVC.view.alpha = 0
        referenceView.isHidden = true // 시작 뷰 숨김
        
        container.addSubview(thumbnailImageView)
        
        UIView.animate(withDuration: config.duration,
                       delay: config.delay,
                       usingSpringWithDamping: config.springWithDamping,
                       initialSpringVelocity: config.initialSpringVelocity,
                       options: [.beginFromCurrentState, .curveEaseInOut]) {
            
            thumbnailImageView.frame = endFrame
            
            toVC.view.alpha = 1.0
            toVC.view.backgroundColor = .white
            
        } completion: { finished in
            targetImageView.alpha = 1
            thumbnailImageView.removeFromSuperview()
            
            self.referenceView.isHidden = false
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }
    }
    
    func animationEnded(_ transitionCompleted: Bool) {
        transitionContext = nil
    }
}

 

이제 본격적인 애니메이션 코드를 확인해볼 차례입니다.

이 부분에서 굉장히 골머리를 앓았는데 끝나고나니 꽤나 직관적인 코드 구성이였습니다.

 

UIViewTransitioningDelegate의 애니메이션은 아래의 순서로 진행됩니다.

 

1. UIKit은 transitioning delegate의 interactionControllerForPresentation: 메소드를 통해 interactive animator 객체가 있는지 보고 nil을 반환하는 경우 interaction 없이 애니메이션을 실행합니다.

2. UIKit은 transitionDuration: 을 호출하여 animation duration을 가져옵니다.

3. UIKit은 animation을 시작하기  위해 적절한 함수를 호출합니다.

- non- interactive 애니메이션을 위해서는 UIKit은 animateTransition: 함수를 호출합니다. // 우린 이쪽

- interactive 애니메이션을 위해서는 UiKit은 startInteractiveTransition를 호출합니다.

4. UIKit은 애니메이션 객체가 context transitioning 객체의 completeTransition: 함수를 호출하는 것을 기다립니다.

커스텀 애니메이터는 전반적으로 해당 함수를 애니메이션이 종료된 호출합니다. 함수를 호출하면 transition 종료되며 UiKit presentViewController:animated: completion completion handler 호출할 잇음을 알려주고 animator 객체 자체의 animationEnded: 함수를 호출합니다.

 

위의 코드를 보면 transitionDuration 함수에 duration을 설정해주었고 animateTransition으로 애니메이션을 구현해주었습니다.

animateTransition 함수에서 보이는 transitionContext는 Transition 할때의 정보를 담고있는 객체로써 유저는 해당 객체를 직접 설정할 수 없습니다.

 

ContainerView는 애니메이션이 실행되는 슈퍼뷰를 나타냅니다.

시작점은 ReferenceView지만 해당 좌표를 ContainerView에 올려줘야 애니메이션이 정상적으로 작동합니다.

referenceView.convert() 함수가 이 부분에 해당합니다.

 

guard let toVC = transitionContext.viewController(forKey: .to),
              let detailVC = toVC as? DetailContentViewController,
              let targetImageViewFrame = detailVC.getContentFrame() else {
            transitionContext.completeTransition(false)
            return
        }

 

 

그 다음 toVC는 TransitionContext에 이동할 종착지를 설정해주는 것 입니다.

위 그림에서 파란색 View에 해당하는 VC가 바로 toVC입니다.

저는 해당 VC를 DetailContentViewController로 구현하였습니다.

 

그리고 제가 설정해야하는 것은 reference뷰가 이동할 좌표입니다.

빨간 Cell의 뷰에서 초록색 뷰의 좌표로 이동해야하는데 초록색 뷰의 좌표는 실제 Image의 비율과 동일해야합니다.

DetailContent 이미지를 표시하는 DetailContentViewController의 ViewModel은 이미지 관련 정보를 담은 FHAsset 데이터를 가지고 있으므로 해당 데이터에서 값을 가져와 비율을 계산해 넣어줍니다.

 

여기까지가 detailVC.getContentFrame() 까지의 작업입니다.

 

 

 

 



























공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/11   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
글 보관함