
본 글은 Customizing the Transition Animations (View Controller Programming Guide for iOS)를 한국어로 번역하여 옮긴 글입니다.
(옮긴이 주: 일부 영단어(예: Presentation 등)는 가독성을 위해 음역하였습니다.)
트랜지션(transition) 애니메이션은 앱의 인터페이스 변화에 대한 시각적인 피드백을 제공합니다. UIKit은 뷰 컨트롤러를 표시할 때 사용할 표준 트랜지션 스타일들을 제공하며, 표준 트랜지션에 더해 직접 만든 자신의 트랜지션을 추가할 수 있습니다.
트랜지션 애니메이션은 하나의 뷰 컨트롤러의 내용을 다른 뷰 컨트롤러의 내용으로 교체합니다. 트랜지션에는 표시(presentation)과 해제(dismissal)라는 두 가지 유형이 있습니다. 표시는 앱의 뷰 컨트롤러 계층에 새로운 뷰 컨트롤러를 추가하는 반면에, 해제는 뷰 컨트롤러 계층에서 하나 이상의 뷰 컨트롤러를 제거합니다.
트랜지션 애니메이션을 구현하려면 많은 객체가 필요합니다. UIKit은 트랜지션에 연관된 모든 객체의 기본 버전을 제공하며, 이를 전부나 일부만 커스터마이징할 수 있습니다. 적절한 객체를 선택하면, 적은 양의 코드로도 애니메이션을 구현할 수 있습니다. UIKit이 제공하는 기존 코드의 이점을 활용한다면, 상호작용이 가능한 애니메이션도 쉽게 구현할 수 있습니다.
트랜지셔닝 델리게이트(transitioning delegate)는 트랜지션 애니메이션과 커스텀 프리젠테이션을 위한 시작점입니다. 트랜지셔닝 델리게이트는 직접 정의하고 UIViewControllerTransitioningDelegate 프로토콜을 준수하는 객체입니다. 그 역할은 UIKit에 아래 객체들을 제공하는 것입니다.
애니메이터 객체(Animator objects). 애니메이터 객체는 뷰 컨트롤러의 뷰가 나타나거나 사라질 때 사용할 애니메이션을 생성해야 합니다. 트랜지셔닝 델리게이트는 뷰 컨트롤러가 표시되거나 사라질 때 사용할 별도의 애니메이터 객체를 제공할 수 있습니다. 애니메이터 객체는 UIViewControllerAnimatedTransitioning 프로토콜을 준수해야 합니다.
상호작용이 가능한 애니메이터 객체(Interactive animator objects). 상호작용이 가능한 애니메이터 객체는 터치 이벤트나 제스처 인식기(gesture recognizer)를 사용해 커스텀 애니메이션의 타이밍을 제어합니다. 상호작용이 가능한 애니메이터 객체는 UIViewControllerInteractiveTransitioning 프로토콜을 준수해야 합니다.
상호작용이 가능한 애니메이터를 생성하는 가장 쉬운 방법은 UIPercentDrivenInteractiveTransition 클래스를 서브클래싱하고, 하위 클래스에 이벤트 처리 코드를 구현하는 것입니다. 이 클래스는 기존 애니메이터 객체를 사용해 생성된 애니메이션의 타이밍을 제어합니다. 직접 상호작용이 가능한 애니메이터를 생성하려면 애니메이션의 각 프레임을 직접 만들어야(render) 합니다.
프리젠테이션 컨트롤러(Presentation Controller). 프리젠테이션 컨트롤러는 뷰 컨트롤러가 화면에 표시되는 동안 프리젠테이션 스타일을 관리합니다. 시스템은 기본 프리젠테이션 스타일을 위한 프리젠테이션 컨트롤러를 제공하며, 직접 만든 프리젠테이션 스타일을 위한 커스텀 프리젠테이션 컨트롤러를 제공할 수 있습니다. 커스텀 프리젠테이션 컨트롤러 생성에 대한 자세한 정보는 Creating Custom Presentations를 참조하세요.
뷰 컨트롤러의 transitioningDelegate 프로퍼티에 트랜지셔닝 델리게이트를 할당하면 UIKit은 커스텀 트랜지션이나 프리젠테이션을 수행합니다. 델리게이트는 제공할 객체를 선택적으로 지정할 수 있습니다. 애니메이터 객체를 제공하지 않으면, UIKit은 뷰 컨트롤러의 modalTransitionStyle 프로퍼티에 지정된 표준 트랜지션 애니메이션을 사용합니다.
그림 10-1은 트랜지셔닝 델리게이트와 표시된(presented) 뷰 컨트롤러의 애니메이터 객체들이 어떤 관계를 가지는지 보여줍니다. 이 프리젠테이션 컨트롤러는 뷰 컨트롤러의 modalPresentationStyle 프로퍼티가 UIModalPresentationCustom으로 설정된 경우에만 사용됩니다.

트랜지셔닝 델리게이트를 구현하는 방법에 대한 자세한 정보는 Implementing the Transitioning을 참조하세요. 트랜지션 델리게이트 객체의 메서드에 대한 자세한 정보는 UIViewControllerTransitioningDelegate Protocol Reference를 참조하세요.
표시되는 뷰 컨트롤러의 transitioningDelegate 프로퍼티에 유효한 객체가 할당되어 있을 때, UIKit은 제공받은 커스텀 애니메이터 객체를 사용해 뷰 컨트롤러를 표시합니다. 매 표시를 할 때마다, UIKit은 커스텀 애니메이터 객체를 가져오기 위해 트랜지셔닝 델리게이트의 animationControllerForPresentedController:presentingController:sourceController: 메서드를 호출합니다. 사용 가능한 객체라면, UIKit은 아래 단계를 수행합니다.
UIKit은 트랜지셔닝 델리게이트의 interactionControllerForPresentation: 메서드를 호출해 상호작용이 가능한 애니메이터 객체가 있는지 확인합니다. 해당 메서드가 nil을 반환하면, UIKit은 사용자 상호작용이 없는 애니메이션을 실행합니다.
UIKit은 애니메이터 객체의 transitionDuration: 메서드를 호출해 애니메이션 지속 시간을 가져옵니다.
UIKit은 아래 메서드를 호출해 애니메이션을 시작합니다.
UIKit은 애니메이터 객체에서 컨텍스트 트랜지셔닝 객체(context transitioning object)의 completeTransition: 메서드가 호출될 때까지 기다립니다.
커스텀 애니메이터는 애니메이션이 끝나면 일반적으로 애니메이션의 완료 블록에서 해당 메서드를 호출합니다. 이 메서드를 호출하면 트랜지션이 종료되며, UIKit은 presentViewController:animated:completion: 메서드의 완료 핸들러와 애니메이터 객체의 animationEnded: 메서드를 호출합니다.
뷰 컨트롤러가 해제될 때, UIKit은 트랜지셔닝 델리게이트의 animationControllerForDismissedController: 메서드를 호출하고, 아래 단계를 수행합니다.
UIKit은 트랜지셔닝 델리게이트의 interactionControllerForDismissal: 메서드를 호출해 상호작용이 가능한 애니메이터 객체가 있는지 확인합니다. 해당 메서드가 nil을 반환하면, UIKit은 사용자 상호작용이 없는 애니메이션을 실행합니다.
UIKit은 애니메이터 객체의 transitionDuration: 메서드를 호출해 애니메이션 지속 시간을 가져옵니다.
UIKit은 아래 메서드를 호출해 애니메이션을 시작합니다.
UIKit은 애니메이터 객체에서 컨텍스트 트랜지셔닝 객체(context transitioning object)의 completeTransition: 메서드가 호출될 때까지 기다립니다.
커스텀 애니메이터는 애니메이션이 끝나면 일반적으로 애니메이션의 완료 블록에서 해당 메서드를 호출합니다. 이 메서드를 호출하면 트랜지션이 종료되며, UIKit은 presentViewController:animated:completion: 메서드의 완료 핸들러와 애니메이터 객체의 animationEnded: 메서드를 호출합니다.
⚪️ IMPORTANT
애니메이션이 완료되면 반드시 completeTransition: 메서드를 호출해야 합니다. UIKit은 해당 메서드를 호출하기 전까지 트랜지션 과정을 종료하지 않으며, 따라서 앱에 제어권을 반환하지 않습니다.
트랜지션 애니메이션을 시작하기 전, UIKit은 트랜지셔닝 컨텍스트 객체(transtioning context object)를 생성하고, 이 객체에 애니메이션을 수행하는 방법에 대한 정보를 채웁니다. 트랜지셔닝 컨텍스트 객체는 코드에서 중요한 부분을 차지합니다. 이 객체는 UIViewControllerContextTransitioning 프로토콜을 구현하며, 트랜지션에 연관된 뷰 컨트롤러와 뷰에 대한 참조를 저장합니다. 또한, 애니메이션이 상호작용한지 여부를 포함해 트랜지션을 수행하는 방법에 대한 정보도 저장합니다. 애니메이터 객체는 실제 애니메이션을 설정하고 실행하기 위해 이 모든 정보를 필요로 합니다.
⚪️ IMPORTANT
커스텀 애니메이션을 설정할 때, 직접 관리해야 하는 모든 캐시된(cached) 정보보다는 항상 트랜지셔닝 컨텍스트 객체가 제공하는 데이터나 객체를 사용해야 합니다. 트랜지션은 다양한 조건에서 발생할 수 있으며, 그 중 일부는 애니메이션 매개변수를 바꿀 수 있습니다. 트랜지셔닝 컨텍스트 객체는 애니메이션을 수행하는 데 필요한 정확한 정보를 가지도록 보장받습니다. 그 반면에 캐시된 정보는 애니메이터의 메서드가 호출될 때쯤에는 오래되어 쓸모가 없어질 수 있습니다.
그림 10-2는 트랜지셔닝 컨텍스트 객체가 다른 객체와 소통하는 방법을 보여줍니다. 애니메이터 객체는 해당 객체를 animateTransition: 메서드에서 받습니다. 생성한 애니메이션은 제공되는 컨테이너 뷰 안에서 이루어져야 합니다. 예를 들어, 뷰 컨트롤러를 표시할 때, 표시되는 뷰 컨트롤러의 뷰는 컨테이너 뷰에 추가해야 합니다. 컨테이너 뷰는 윈도우일 수도 있고 일반 뷰일 수도 있지만, 항상 애니메이션이 실행되도록 구성됩니다.

트랜지션 컨텍스트 객체에 대한 자세한 정보는 UIViewControllerContextTransitioning Protocol Reference를 참조하세요.
기본 트랜지션과 커스텀 트랜지션 모두 UIKit은 추가로 필요한 애니메이션을 실행하기 위해 트랜지션 코디네이터 객체를 생성합니다. 뷰-컨트롤러가 표시되고 해제될 때를 제외하고, 트랜지션은 인터페이스가 회전을 하거나 뷰 컨트롤러의 프레임이 변할 때 일어날 수 있습니다. 이 모든 트랜지션은 뷰 계층 구조의 변경을 의미합니다. 트랜지션 코디네이터는 이러한 변경 사항을 추적하고 동시에 컨텐츠를 애니메이션할 수 있는 방법입니다. 트랜지션 코디네이터에 접근하려면, 표시와 해제에 연관된 뷰 컨트롤러의 transitionCoordinator 프로퍼티에서 객체를 가져오세요. 트랜지션 코디네이터는 트랜지션이 진행되는 동안에만 존재합니다.
그림 10-3은 트랜지션 코디네이터와 프리젠테이션에 연관된 뷰 컨트롤러 간의 관계를 보여줍니다. 트랜지션에 대한 정보를 얻고 트랜지션 애니메이션과 동시에 실행할 애니메이션 블록을 등록하려면 트랜지션 코디네이터를 활용하세요. 트랜지션 코디네이터 객체는 타이밍 정보, 애니메이션의 현재 상태와 트랜지션에 연관된 뷰와 뷰 컨트롤러를 제공하는 UIViewControllerTransitioningCoordinatorContext 프로토콜을 준수합니다. 애니메이션 블록이 실행될 때, 동일한 정보를 가지는 컨텍스트 객체를 비슷하게 전달받습니다.

트랜지션 코디네이터 객체에 대한 자세한 정보는 UIViewControllerTransitioningCoordinator Protocol Reference를 참조하세요. 애니메이션을 구성하는 데 사용할 수 있는 문맥상의(contextual) 정보에 대한 자세한 정보는 UIViewControllerTransitioningCoordinatorContext Protocol Reference를 참조하세요.
커스텀 애니메이션을 사용해 뷰 컨트롤러를 표시하려면, 기존 뷰 컨트롤러의 액션 메서드에서 아래 작업을 수행하세요.
표시하고자 하는 뷰 컨트롤러를 생성하세요.
커스텀 트랜지션 델리게이트 객체를 생성하고, 이 객체를 뷰 컨트롤러의 transitioningDelegate 프로퍼티에 할당하세요. 트랜지션 델리게이트의 메서드는 요청 시 커스텀 애니메이터 객체를 생성하고 반환합니다.
뷰 컨트롤러를 표시하기 위해 presentViewController:animated:completion: 메서드를 호출하세요.
presentViewController:animated:completion: 메서드를 호출하면, UIKit은 표시 과정을 시작합니다. 프리젠테이션은 다음 런-루프 반복 중에 시작되며, 커스텀 애니메이터가 completeTranstion: 메서드를 호출할 때까지 계속됩니다. 상호작용이 가능한 트랜지션은 트랜지션이 진행되는 동안 터치 이벤트를 처리할 수 있지만, 상호작용이 불가능한(noninteractive) 트랜지션은 애니메이터 객체가 지정한 시간 동안 실행됩니다.
트랜지셔닝 델리게이트의 목적은 커스텀 객체를 생성하고 반환하는 것입니다. 아래 10-1 코드는 트랜지셔닝 메서드의 구현이 얼마나 간단할 수 있는지 보여줍니다. 이 예제는 커스텀 애니메이터 객체를 생성하고 반환합니다. 실제 작업의 대부분은 애니메이터 객체 자체에서 직접 처리합니다.
- (id<UIViewControllerAnimatedTransitioning>)
animationControllerForPresentedController:(UIViewController *)presented
presentingController:(UIViewController *)presenting
sourceController:(UIViewController *)source {
MyAnimator* animator = [[MyAnimator alloc] init];
return animator;
}
(옮긴이 주: 아래 코드는 위 예제를 Swift로 변환한 코드입니다.)
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController) -> (any UIViewControllerAnimatedTransitioning)? {
let animator = MyAnimator()
return animator
}
트랜지셔닝 델리게이트의 다른 메서드도 앞의 예제에서 나온 메서드만큼 간단할 수 있습니다. 또한, 앱의 현재 상태에 따라 다른 애니메이터 객체를 반환하도록 커스텀 로직을 구성할 수 있습니다. 트랜지션 델리게이트의 메서드에 대한 자세항 정보는 UIViewControllerTransitioningDelegate Protocol Reference를 참조하세요.
애니메이터 객체는 UIViewControllerAnimatedTransitioning 프로토콜을 채택하는 객체입니다. 애니메이터 객체는 정해진 시간 동안 실행되는 애니메이션을 생성합니다. 애니메이터 객체의 핵심은 실제 애니메이션을 생성하는 데 사용하는 animateTransition: 메서드입니다. 애니메이션 과정은 대략 아래와 같은 단계로 나눌 수 있습니다.
애니메이션 매개변수를 가져옵니다.
코어 애니메이션(core animation)이나 UIView 애니메이션 메서드를 사용해 애니메이션을 생성합니다.
트랜지션을 완료하고 정리합니다.
animateTransition: 메서드에 전달되는 컨텍스트 트랜지셔닝 객체는 애니메이션을 실행할 때 사용하는 데이터를 포함하고 있습니다. 컨텍스트 트랜지셔닝 객체로부터 더 최신의 정보를 얻을 수 있다면, 절대로 캐시된 정보나 뷰 컨트롤러로부터 정보를 가져와 사용하면 안됩니다. 뷰 컨트롤러를 표시하고 해제하는 작업은 때때로 뷰 컨트롤러의 외부 객체를 포함할 수 있습니다. 예를 들어, 커스텀 프리젠테이션 컨트롤러는 프리젠테이션의 일부로 백그라운드 뷰를 추가할 수 있습니다. 컨텍스트 트랜지셔닝 객체는 추가적인 뷰와 객체를 고려하며 애니메이션을 위한 적절한 뷰를 제공합니다.
viewControllerForKey: 메서드를 두 번 호출해 트랜지션과 관련된 "from"과 "to" 뷰 컨트롤러를 가져오세요. 절대로 트랜지션에 어떤 뷰 컨트롤러가 참여하는지 알고 있다고 가정하지 마세요. UIKit은 새로운 트레잇 환경(trait environment)으로 변경하거나 앱의 요청에 응답하는 과정에서 뷰 컨트롤러를 변경할수 있습니다.
containerView 메서드를 호출해 애니메이션을 위한 상위 뷰를 가져오세요. 모든 핵심 하위 뷰를 해당 뷰에 추가하세요. 예를 들어, 프리젠테이션 중 표시되는 뷰 컨트롤러의 뷰를 해당 뷰에 추가하세요.
viewForKey: 메서드를 호출해 추가되거나 제거될 뷰를 가져오세요. 트랜지션 중 뷰 컨트롤러의 뷰만 추가되거나 제거되는 것이 아닐 수 있습니다. 프리젠테이션 컨트롤러는 추가되거나 제거해야 할 뷰를 계층 구조에 삽입할 수 있습니다. viewForKey: 메서드는 추가 또는 제거에 필요한 모든 것을 포함한 최상위 뷰를 반환합니다.
finalFrameForViewController: 메서드를 호출해 추가되거나 제거될 뷰의 최종 프레임을 가져오세요.
컨텍스트 트랜지셔닝 객체는 "from"과 "to" 명명법을 사용해 뷰 컨트롤러, 뷰와 트랜지션에 연관된 프레임을 식별합니다. "from" 뷰 컨트롤러는 항상 트랜지션의 시작 시 화면에 표시되어 있는 뷰 컨트롤러의 뷰입니다. 그리고 "to" 뷰 컨트롤러는 트랜지션의 종료 시 보이게 될 뷰 컨트롤러의 뷰입니다. 아래 10-4 그림에서 보이듯이, "from"과 "to" 뷰 컨트롤러는 표시와 해제 과정에서 역할이 뒤바뀝니다.

역할의 뒤바뀜은 표시와 해제 모두 처리하는 단일 애니메이터를 더 쉽게 작성할 수 있게 합니다. 애니메이터를 설계할 때, 표시 과정을 애니메이션하는지 해제 과정을 애니메이션하는지 알 수 있는 프로퍼티만 포함하면 됩니다. 둘 사이에서 반드시 필요한 차이점은 다음과 같습니다.
*해제를 하는 경우, "from" 뷰를 컨테이너 뷰 계층 구조에서 제거하세요.
일반적인 프리젠테이션 중에는 표시되는 뷰 컨트롤러에 속하는 뷰가 적절한 위치로 애니메이션됩니다. 다른 뷰가 프리젠테이션의 일부로 애니메이션이 될 수 있지만, 애니메이션의 주요 목표는 항상 뷰 계층 구조에 추가되는 뷰입니다.
주요 뷰를 애니메이션할 때, 애니메이션 구성을 위한 기본 동작은 동일합니다. 트랜지셔닝 컨텍스트 객체로부터 필요한 객체와 데이터를 가져오고, 해당 정보를 실제 애니메이션을 생성하는데 활용하세요.
viewControllerForKey:와 viewForKey: 메서드를 사용해 트랜지션에 연관된 뷰 컨트롤러와 뷰를 가져오세요.
"to" 뷰의 시작 위치을 설정하세요. 다른 프로퍼티들도 시작 값을 설정하세요.
컨텍스트 트랜지셔닝 객체의 [finalFrameForViewController:]https://developer.apple.com/documentation/uikit/uiviewcontrollercontexttransitioning/1622024-finalframe() 메서드로 "to" 뷰의 최종 위치를 가져오세요.
"to" 뷰를 컨테이너 뷰의 하위 뷰로 추가하세요.
애니메이션을 생성하세요.
해제 애니메이션:
* [viewControllerForKey:](https://developer.apple.com/documentation/uikit/uiviewcontrollercontexttransitioning/1622043-viewcontrollerforkey)와 [viewForKey:](https://developer.apple.com/documentation/uikit/uiviewcontrollercontexttransitioning/1622055-view) 메서드를 사용해 트랜지션에 연관된 뷰 컨트롤러와 뷰를 가져오세요.
* "from" 뷰의 최종 위치를 계산하세요. 이 뷰는 해제되고 있는 표시된 뷰 컨트롤러에 속합니다.
그림 10-5는 뷰를 대각선으로 애니메이션하는 표시 및 해제 트랜지션을 보여줍니다. 프리젠테이션 중에 표시되는 뷰는 화면 바깥에서 시작해 좌상단으로 비스듬히 이동하며 화면에 나타날 때까지 애니메이션됩니다. 해제 중에 뷰는 방향을 반전해 우하단으로 이동하며 화면에 사라질 때까지 애니메이션됩니다.

아래 10-2 코드는 그림 10-5에 보이는 트랜지션을 구현하는 방법을 보여줍니다. 애니메이션에 필요한 객체를 가져온 뒤, animateTransition: 메서드에서 대상 뷰의 프레임을 계산합니다. 프리젠테이션 중에 표시되는 뷰는 toView 변수로 표현됩니다. 해제 중에 해제되는 뷰는 fromView 변수로 표현됩니다. presenting 프로퍼티는 애니메이터 객체 자체에 정의된 프로퍼티이며, 트랜지셔닝 델리게이트가 애니메이터를 생성할 때 적절한 값으로 설정합니다.
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
// Get the set of relevant objects.
UIView *containerView = [transitionContext containerView];
UIViewController *fromVC = [transitionContext
viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext
viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
// Set up some variables for the animation.
CGRect containerFrame = containerView.frame;
CGRect toViewStartFrame = [transitionContext initialFrameForViewController:toVC];
CGRect toViewFinalFrame = [transitionContext finalFrameForViewController:toVC];
CGRect fromViewFinalFrame = [transitionContext finalFrameForViewController:fromVC];
// Set up the animation parameters.
if (self.presenting) {
// Modify the frame of the presented view so that it starts
// offscreen at the lower-right corner of the container.
toViewStartFrame.origin.x = containerFrame.size.width;
toViewStartFrame.origin.y = containerFrame.size.height;
}
else {
// Modify the frame of the dismissed view so it ends in
// the lower-right corner of the container view.
fromViewFinalFrame = CGRectMake(containerFrame.size.width,
containerFrame.size.height,
toView.frame.size.width,
toView.frame.size.height);
}
// Always add the "to" view to the container.
// And it doesn't hurt to set its start frame.
[containerView addSubview:toView];
toView.frame = toViewStartFrame;
// Animate using the animator's own duration value.
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
if (self.presenting) {
// Move the presented view into position.
[toView setFrame:toViewFinalFrame];
}
else {
// Move the dismissed view offscreen.
[fromView setFrame:fromViewFinalFrame];
}
}
completion:^(BOOL finished){
BOOL success = ![transitionContext transitionWasCancelled];
// After a failed presentation or successful dismissal, remove the view.
if ((self.presenting && !success) || (!self.presenting && success)) {
[toView removeFromSuperview];
}
// Notify UIKit that the transition has finished
[transitionContext completeTransition:success];
}];
}
(옮긴이 주: 아래 코드는 위 예제를 Swift로 변환한 코드입니다.)
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Get the set of relevant objects.
guard let containerView = transitionContext.containerView as UIView?,
let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to),
let toView = transitionContext.view(forKey: .to),
let fromView = transitionContext.view(forKey: .from) else {
return
}
// Set up some variables for the animation.
let containerFrame = containerView.frame
var toViewStartFrame = transitionContext.initialFrame(for: toVC)
let toViewFinalFrame = transitionContext.finalFrame(for: toVC)
var fromViewFinalFrame = transitionContext.finalFrame(for: fromVC)
// Set up the animation parameters.
if presenting {
// Modify the frame of the presented view so that it starts
// offscreen at the lower-right corner of the container.
toViewStartFrame.origin.x = containerFrame.size.width
toViewStartFrame.origin.y = containerFrame.size.height
} else {
// Modify the frame of the dismissed view so it ends in
// the lower-right corner of the container view.
fromViewFinalFrame = CGRect(x: containerFrame.size.width,
y: containerFrame.size.height,
width: toView.frame.size.width,
height: toView.frame.size.height)
}
// Always add the "to" view to the container.
// And it doesn't hurt to set its start frame.
containerView.addSubview(toView)
toView.frame = toViewStartFrame
// Animate using the animator's own duration value.
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
if self.presenting {
// Move the presented view into position.
toView.frame = toViewFinalFrame
} else {
// Move the dismissed view offscreen.
fromView.frame = fromViewFinalFrame
}
}) { finished in
let success = !transitionContext.transitionWasCancelled
// After a failed presentation or successful dismissal, remove the view.
if (self.presenting && !success) || (!self.presenting && success) {
toView.removeFromSuperview()
}
// Notify UIKit that the transition has finished
transitionContext.completeTransition(success)
}
}
트랜지션 애니메이션이 끝난 후, completeTransition: 메서드를 호출하는 것이 중요합니다. 해당 메서드를 호출하면 트랜지션이 완료되었으며, 사용자가 표시된 뷰 컨트롤러을 사용할 수 있음을 UIKit에게 알려줍니다. 해당 메서드를 호출하면 presentViewController:animated:completion: 메서드의 완료 핸들러와 애니메이터 객체의 animationEnded: 메서드를 포함한 여러 완료 핸들러가 연쇄적으로 실행됩니다. completeTransition: 메서드를 호출하기 가장 적합한 위치는 애니메이션 블록의 완료 핸들러 내부입니다.
트랜지션은 취소될 수 있기 때문에, 필요한 정리 작업을 결정하기 위해 컨텍스트 객체의 transitionWasCancelled 메서드가 반환하는 값을 사용해야 합니다. 프리젠테이션이 취소되었을 때, 애니메이터는 뷰 계층 구조에 가한 모든 수정을 반드시 원래대로 되돌려야 합니다. 성공적인 해제에도 유사한 작업이 필요합니다.
상호작용이 가능한 애니메이션을 만드는 가장 쉬운 방법은 UIPercentDrivenInteractiveTransition 객체를 사용하는 것입니다. UIPercentDrivenInteractiveTransition 객체는 기존 애니메이터 객체와 함께 작동하여 애니메이션의 타이밍을 제어할 수 있도록 합니다. 사용자가 제공하는 완료 비율 값을 활용하여 이를 수행합니다. 완료 비율 값을 계산하는 이벤트-처리 코드를 작성하고, 새로운 이벤트가 발생할 때마다 이를 업데이트하세요.
UIPercentDrivenInteractiveTransition 클래스를 서브클래싱하여 사용하거나, 그대로 사용할 수 있습니다. 서브클래싱을 한다면, 하위 클래스의 init 메서드(또는 startInteractiveTransition: 메서드)를 사용해 이벤트-처리 코드를 한번 설정하세요. 이후, 커스텀 이벤트-처리 코드로 새로운 완료 비율 값을 계산하고, updateInteractiveTransition: 메서드를 호출하세요. 코드가 트랜지션을 완료해야 한다고 판단하면, finishInteractiveTransition 메서드를 호출하세요.
아래 10-3 코드는 UIPercentDrivenInteractiveTransition 하위 클래스의 startInteractiveTransition: 메서드의 커스텀 구현을 보여줍니다. 이 메서드는 터치 이벤트를 추적하기 위해 팬-제스처 인식기를 생성하고, 애니메이션을 처리하기 위해 이를 컨테이너 뷰에 추가합니다. 또한, 나중에 사용될 수 있도록 트랜지션 컨텍스트에 대한 참조를 저장합니다.
- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
// Always call super first.
[super startInteractiveTransition:transitionContext];
// Save the transition context for future reference.
self.contextData = transitionContext;
// Create a pan gesture recognizer to monitor events.
self.panGesture = [[UIPanGestureRecognizer alloc]
initWithTarget:self action:@selector(handleSwipeUpdate:)];
self.panGesture.maximumNumberOfTouches = 1;
// Add the gesture recognizer to the container view.
UIView* container = [transitionContext containerView];
[container addGestureRecognizer:self.panGesture];
}
(옮긴이 주: 아래 코드는 위 예제를 Swift로 변환한 코드입니다.)
override func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
// Always call super first.
super.startInteractiveTransition(transitionContext)
// Save the transition context for future reference.
self.contextData = transitionContext
// Create a pan gesture recognizer to monitor events.
self.panGesture = UIPanGestureRecognizer(target: self, action: #selector(handleSwipeUpdate(_:)))
self.panGesture.maximumNumberOfTouches = 1
// Add the gesture recognizer to the container view.
if let container = transitionContext.containerView {
container.addGestureRecognizer(self.panGesture)
}
}
제스처 인식기는 새로운 이벤트가 발생할 때마다 해당 액션 메서드를 호출합니다. 액션 메서드의 구현은 제스처가 성공했는지, 실패했는지, 아니면 여전히 진행 중인 상태인지 결정하기 위해 제스처 인식기의 상태 정보를 활용합니다. 그와 동시에, 제스처에 대한 새로운 비율 값을 계산하기 위해 가장 최근의 터치 이벤트 정보를 활용할 수 있습니다.
아래 10-4 코드는 10-3 코드에서 구성한 팬-제스처 인식기가 호출하는 메서드를 보여줍니다. 새로운 이벤트가 발생할 때마다, 이 메서드는 애니메이션의 완료 비율을 계산하기 위해 수직 이동 거리를 사용합니다. 제스처가 끝나면, 메서드는 트랜지션을 완료합니다.
-(void)handleSwipeUpdate:(UIGestureRecognizer *)gestureRecognizer {
UIView* container = [self.contextData containerView];
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
// Reset the translation value at the beginning of the gesture.
[self.panGesture setTranslation:CGPointMake(0, 0) inView:container];
}
else if (gestureRecognizer.state == UIGestureRecognizerStateChanged) {
// Get the current translation value.
CGPoint translation = [self.panGesture translationInView:container];
// Compute how far the gesture has travelled vertically,
// relative to the height of the container view.
CGFloat percentage = fabs(translation.y / CGRectGetHeight(container.bounds));
// Use the translation value to update the interactive animator.
[self updateInteractiveTransition:percentage];
}
else if (gestureRecognizer.state >= UIGestureRecognizerStateEnded) {
// Finish the transition and remove the gesture recognizer.
[self finishInteractiveTransition];
[[self.contextData containerView] removeGestureRecognizer:self.panGesture];
}
}
(옮긴이 주: 아래 코드는 위 예제를 Swift로 변환한 코드입니다.)
@objc func handleSwipeUpdate(_ gestureRecognizer: UIGestureRecognizer) {
guard let container = contextData?.containerView else { return }
switch gestureRecognizer.state {
case .began:
// Reset the translation value at the beginning of the gesture.
panGesture?.setTranslation(.zero, in: container)
case .changed:
// Get the current translation value.
let translation = panGesture?.translation(in: container) ?? .zero
// Compute how far the gesture has travelled vertically,
// relative to the height of the container view.
let percentage = abs(translation.y / container.bounds.height)
// Use the translation value to update the interactive animator.
updateInteractiveTransition(percentage)
case .ended, .cancelled:
// Finish the transition and remove the gesture recognizer.
finishInteractiveTransition()
container.removeGestureRecognizer(panGesture!)
default:
break
}
}
⚪️ NOTE
계산한 값은 애니메이션의 전체 길이에 대한 완료 비율을 나타냅니다. 상호작용이 가능한 애니메이션의 경우, 초기 속도(initial velocities), 감쇠 값(damping values)과 비선형 완료 곡선과 같은 비선형 효과를 피하는 것이 좋습니다. 이 같은 효과는 이벤트의 터치 위치와 하위 뷰의 움직임 간의 연계를 약화시키는 경향이 있기 때문입니다.
트랜지션에 참여하는 뷰 컨트롤러는 프리젠테이션이나 트랜지션 애니메이션 위에 추가적인 애니메이션을 수행할 수 있습니다. 예를 들어, 표시되는 뷰 컨트롤러는 트랜지션 중에 자신의 뷰 계층 구조를 애니메이션하거나, 트랜지션이 진행되는 동안 모션 효과나 다른 시각적인 피드백을 추가할 수 있습니다. 표시하거나 표시되는 뷰 컨트롤러의 transitionCoordinator 프로퍼티에 접근할 수 있는 한, 모든 객체는 애니메이션을 생성할 수 있습니다. 트랜지션 코디네이터는 트랜지션이 진행되는 동안에만 존재합니다.
애니메이션을 생성하려면, 트랜지션 코디네이터의 animateAlongsideTransition:completion: 또는 animateAlongsideTransitionInView:animation:completion: 메서드를 호출하세요. 제공하는 블록은 트랜지션 애니메이션이 시작될 때까지 저장되며, 애니메이션이 시작되면 트랜지션 애니메이션의 나머지 부분과 함께 실행됩니다.
커스텀 프리젠테이션의 경우, 표시된 뷰 컨트롤러에 커스텀 디자인(appearance)을 적용하기 위해 커스텀 프리젠테이션 컨트롤러를 제공할 수 있습니다. 프리젠테이션 컨트롤러는 뷰 컨트롤러와 그 컨텐츠와는 별도로 분리된 커스텀 UI 요소(chrome)를 관리합니다. 예를 들어, 뷰 컨트롤러의 뷰 뒤에 배치된 흐림 효과(dimming) 뷰는 프리젠테이션 컨트롤러가 관리합니다. 특정 뷰 컨트롤러의 뷰를 관리하지 않는다는 사실은 앱의 모든 뷰 컨트롤러가 동일한 프리젠테이션 컨트롤러를 사용할 수 있다는 걸 의미합니다.
표시된 뷰의 트랜지셔닝 델리게이트를 통해 커스텀 프리젠테이션 컨트롤러를 제공합니다. (뷰 컨트롤러의 modalTransitionStyle 프로퍼티는 UIModalPresentationCustom이 되어야 합니다.) 프리젠테이션 컨트롤러는 다른 애니메이터 객체와 병렬(parallel)로 작동합니다. 애니메이터 객체가 뷰 컨트롤러의 뷰를 어느 위치로 애니메이션하는 동안, 프리젠테이션 컨트롤러는 추가적인 뷰를 어느 위치로 애니메이션합니다. 트랜지션이 끝날 때, 프리젠테이션 컨트롤러는 뷰 계층 구조에 대한 최종 조정을 수행할 수 있습니다.
커스텀 프리젠테이션 컨트롤러를 생성하는 방법에 대한 자세한 정보는 Creating Custom Presentations를 참조하세요.