본문 바로가기
📱iOS

Custom Navigation 전환 애니메이션

by akwlak 2022. 12. 6.

앱스토어를 보면 이런 멋진 애니메이션들로 화면이 present 되거나 push 되는 것을 볼 수 있습니다.

 

이런 애니메이션은 어떻게 만들 수 있을까요

 

그냥 pushViewController를 하게되면 이런 모습이 나오죠.

 

 

이걸 이렇게 바꿔보겠습니다. 앱스토어에 비해 단순하지만, 이해를 위해 간단히 만들어보겠습니다.

 

 

여기서 우리는 크게 두가지를 해주면 됩니다.

 

1. 메인 ViewController를 Navigation의 Delegate으로 만들고 Navigation한테 Animator 넘겨주기

 

2. 넘겨줄 Animator 구현하기

 

Navigation은 화면을 push 하거나 pop 할 때 자신의 delegate한테 animator를 요구하게 됩니다.

 

그리고 이때 nil이 넘어온다면 위에서 본 기본 애니메이션이 실행됩니다.

 

그러니까 기본 애니메이션이 아닌 Custom애니메이션을 사용하고 싶다면

 

여기서 자신만의 animator를 넘겨주면 되는 것이죠.

 

나머지는 코드를 보면서 설명하겠습니다.

 

일단 바로 animator를 정의하겠습니다.

final class MyAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
    // 애니메이션 시간
    private let duration = 0.4
    // push인지 pop인지
    private let operation: UINavigationController.Operation
    // 메인 VC
    private let VC: ViewController
    // 보여지는 VC
    private let presentedVC: PresentedViewController
    
    
    init?(
        operation: UINavigationController.Operation,
        VC: ViewController,
        presentedVC: PresentedViewController) {
            
            self.operation = operation
            self.VC = VC
            self.presentedVC = presentedVC
        }
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        
        return duration
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
      
      	// 여기서 애니메이션 구현!!
    }
}

 

이런식으로 UIViewControllerAnimatedTransitioning를 채택해서 구현할 수 있습니다.

 

여기서는 

 

애니메이션이 진행될 시간을 넘겨주고

 

애니메이션을 구현하면 됩니다.

 

애니메이션을 구현하기 전에

 

메인 뷰컨트롤러를 NavigationController의 Delegate으로 만들어 주겠습니다.

extension ViewController: UINavigationControllerDelegate {
    
    func navigationController(
        _ navigationController: UINavigationController,
        animationControllerFor operation: UINavigationController.Operation,
        from fromVC: UIViewController,
        to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            
            switch operation {
            case .push:
                guard let fromVC = fromVC as? ViewController,
                      let toVC = toVC as? PresentedViewController else { return nil }
                
                let animator = MyAnimator(
                    operation: operation,
                    VC: fromVC,
                    presentedVC: toVC                    
                )
                
                return animator
            case .pop:
                guard let fromVC = fromVC as? PresentedViewController,
                      let toVC = toVC as? ViewController else { return nil }
                
                let animator = MyAnimator(
                    operation: operation,
                    VC: toVC,
                    presentedVC: fromVC
                )
                
                return animator
            default: return nil
                
            }
            
        }
}

 

굉장히 복잡해보이네요..

 

하지만 하는 동작은 단순합니다.

 

push인지 pop인지에 따라 animator를 init 해주고 있는 거죠.

 

fromVCtoVC는 push일 때와 pop일 때 달라질 수 있으므로 이렇게 나눠주었습니다.

 

그리고 viewDidLoad에서 delegate을 설정해 주면 됩니다.

 

navigationController?.delegate = self

 

이제 다시 animation을 구현하러 가겠습니다.

 

어떻게 해줘야 할까요?

 

우선 animateTransition함수에 transitionContext가 있으니 이 친구가 뭔지 알아보겠습니다.

 

공식문서에는

 

A context object encapsulates information about the views and view controllers involved in the transition.

 

번역하면 Transition에 관련된 View와 ViewController들에 대한 정보들을 가지고 있다는 것 같네요.

 

여기서 필요한 View와 ViewController를 빼서 사용하면 되겠습니다.

 

그리고 여기에는 containerView라는 것도 포함이 됩니다.

 

이 친구는 무슨 친구일까요?

 

공식문서에는

 

The container view acts as the superview of all other views (including those of the presenting and presented view controllers) during the animation sequence.

 

번역하면 animation이 진행되는 동안 다른 뷰들의 superview

처럼 동작한다고 하네요.

 

그리고

 

The animator object is responsible for adding the view of the presented view controller, and the animator object or presentation controller must use this view as the container for all other views involved in the transition.

 

animator는 여기에 보일 viewController에 view를 넣어줘야 하고,

 

transition에 포함된 뷰들을 여기서 사용해야 된다고 합니다.

 

그렇군요, 일단 여기까지 코드를 짜 보겠습니다.

 

// 애니메이션이 진행되는 뷰
let container = transitionContext.containerView

// 보여질 뷰 push인지 pop인지에 따라 달라진다
guard let toView = operation == .push ? presentedVC.view : VC.view else {
    // completeTransition을 꼭 불러줘야 한다. false면 실패 true면 성공.            
    transitionContext.completeTransition(false)
    return
}

container.addSubview(toView)
toView.alpha = 0

 

container를 가져오고

 

보일 뷰를 push인지 pop인지에 따라 구분해서 가져온 뒤

 

container에 addSubview 해주었습니다.

 

그리고 보일 뷰가 바로 보이면 안 되고 애니메이션이 끝나고 보여야 하므로 alpha를 0으로 줬습니다.

 

그리고 이제 transition에서 사용될 뷰를 가져오겠습니다.

 

여기서는 저 pink Box가 됩니다.

 

// 애니메이션할 뷰 스냅샷
let animateSnapshot: UIView

// push인지 pop인지에 따라 달라진다
// 스냅샷을 찍고 원래 뷰는 숨긴다. 움직이는 것처럼 보이게 하기 위해
if operation == .push {
    VC.box.alpha = 0
    animateSnapshot = VC.box.snapshotView(afterScreenUpdates: false)!
} else {
    presentedVC.box.alpha = 0
    animateSnapshot = presentedVC.box.snapshotView(afterScreenUpdates: false)!
}

// 애니메이션할 스냅샷을 container에 추가
[animateSnapshot].forEach { container.addSubview($0) }

 

그냥 가져오지 않고 편하게 사용한 뒤 remove 하기 위해  snapshot으로 가져오겠습니다.

 

그리고 움직이는 것처럼 보이게 하기 위해 진짜 뷰는 alpha를 0으로 하겠습니다.

 

그렇지 않으면 복사해서 움직이는 것처럼 보이겠죠?

 

원하는 동작에 따라 달라질 수 있습니다.

 

그리고 container에서 사용해야 하기 때문에 addSubview도 해주었습니다.

 

이제 이 box를 움직일 frame을 가져오겠습니다.

 

// 스냅샷이 위치하게 될 frame window 좌표계로 바꿔준다 nil이면 window
let boxViewRect = VC.box.convert(VC.box.bounds, to: nil)
let presentedBoxRect = presentedVC.box.convert(presentedVC.boxFrame(), to: nil)

 

여기서 convert좌표계를 바꿔주지 않으면 이상하게 동작하게 됩니다.

 

서로 다른 좌표계에 있는 친구들이었으니까요.

 

window로 둘 다 바꿔주었습니다. nil이면 window가 기본값이라고 합니다.

 

전 여기서 이 사실을 몰라 엄청난 삽질을 했었죠..

 

그리고 스냅샷의 처음 프레임을 잡아주겠습니다.

 

// 스냅샷의 처음 프레임을 잡아준다.
[animateSnapshot].forEach {
    $0.frame = operation == .push ? boxViewRect : presentedBoxRect
}

 

push인지 pop인지에 따라 위치가 달라지겠죠?

 

이제 세팅은 모두 끝났습니다.

 

애니메이션만 해주면 됩니다.

 

// 애니메이션
UIView.animate(
    withDuration: duration,
    delay: 0) {
        // 스냅샷의 마지막 프레임을 잡아준다.
        animateSnapshot.frame = self.operation == .push ? presentedBoxRect : boxViewRect
    } completion: { _ in
        // 애니메이션이 끝나고, 스냅샷을 없애고, alpha값들을 제대로 돌려 놓는다.
        animateSnapshot.removeFromSuperview()
        toView.alpha = 1
        self.VC.box.alpha = 1
        self.presentedVC.box.alpha = 1
        transitionContext.completeTransition(true)
    }

 

이렇게 위에서 설정한 duration을 주고, 프레임을 바꿔줍니다.

 

마찬가지로 push인지 pop인지에 따라 달라지겠죠?

 

그리고 completion에서

 

스냅샷을 지워주고 alpha값을 제대로 돌려주면 끝나게 됩니다!

 

 

 

이 플로우를 이해하기가 참 힘들었는데요,

 

간단하게 만들어보니까 어느 정도 감이 잡히고, 아주 요긴하게 쓸 것 같습니다.

 

하지만 아직 부족한 부분이 있습니다. 스와이프로 뒤로가기가 아직 안되기 때문이죠.

 

다음글에서 구현해보겠습니다.

 

전체 코드

https://github.com/12251145/CustomNavigationTransition

 

GitHub - 12251145/CustomNavigationTransition

Contribute to 12251145/CustomNavigationTransition development by creating an account on GitHub.

github.com

 

'📱iOS' 카테고리의 다른 글

CollectionView Custom Layout  (1) 2022.12.05
Dynamic Height Cell 만들기 - CollectionView + CompositionalLayout  (0) 2022.11.30