저번 1편에서는 커스텀 트랜지션을 사용하고 싶을 때
필요한 프로토콜들과
프로토콜 내부에서 제공되는 메서드 및 프로퍼티를 확인하였다.
이번에는 완성된 동영상을 먼저 올리고,
이와 같은 커스텀 트랜지션을 만드는 데 아주 최소한의 필요했던 내용과 이해를 적어보고자 한다.
일단, 완성본은 이러한 형태가 될 것이다.
커스텀 트랜지션을 만들기위해서는
UIViewControllerTransitioningDelegate를 구현한 객체가 존재해야한다.
UIViewControllerTransitioningDelegate를 구현하면
각 Present/Dismiss때 사용할 애니메이터 객체를 반환해줘야 한다.
ViewController에서 바로 구현할 수 있지만, 난 어떠한 뷰 컨트롤러에서 사용할 수 있도록
TransitioningDelegate를 구현한 객체를 하나 만들었다.
final class CustomTransition: NSObject {
private let config: CustomTransitionConfiguration
private let referenceView: UIView
private let presentingTransitionAnimator: CustomPresentingTransitionAnimator
private let dismissalTransitionAnimator: CustomDismissalTransitionAnimator
private var presentationController: UIPresentationController?
private var currentTranslationY: CGFloat = 0
// MARK: - Initializers
init(
referenceView: UIView,
config: CustomTransitionConfiguration = CustomTransitionConfiguration()
) {
self.referenceView = referenceView
self.config = config
presentingTransitionAnimator = CustomPresentingTransitionAnimator(
config: config,
referenceView: referenceView
)
dismissalTransitionAnimator = CustomDismissalTransitionAnimator(
config: config,
referenceView: referenceView
)
}
}
// MARK: - Extension - UIViewControllerTransitioningDelegate
extension CustomTransition: UIViewControllerTransitioningDelegate {
// 뷰 컨트롤러를 프레젠팅할 때 트랜지션 애니메이터 객체를 요청하는 메서드
func animationController(
forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
print("\(#function)")
return presentingTransitionAnimator
}
// 뷰 컨트롤러를 디스미스할 때 프렌지션 애니메이터 객체를 요청하는 메서드
func animationController(
forDismissed dismissed: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
print("\(#function)")
return dismissalTransitionAnimator
}
// 뷰 컨트롤러를 프레젠팅할 때 인터렉티브 애니메이터 객체를 요청하는 메서드
func interactionControllerForPresentation(
using animator: UIViewControllerAnimatedTransitioning
) -> UIViewControllerInteractiveTransitioning? {
print("\(#function)")
return nil
}
// 뷰 컨트롤러를 디스미스할 때 인터렉티브 애니메이터 객체를 요청하는 메서드
func interactionControllerForDismissal(
using animator: UIViewControllerAnimatedTransitioning
) -> UIViewControllerInteractiveTransitioning? {
print("\(#function)")
return dismissalTransitionAnimator
}
// 뷰 컨트롤러를 프레젠팅할 때 뷰 하이러키를 관리하기 위해 사용되는 커스텀 프레젠테이션 컨트롤러를 요청하는 메서드
func presentationController(
forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController
) -> UIPresentationController? {
print("\(#function)")
let presentationController = UIPresentationController(
presentedViewController: presented,
presenting: source
)
let panGestureRecognizer = UIPanGestureRecognizer(
target: self,
action: #selector(handlePanGesture(_:))
)
presented.view.addGestureRecognizer(panGestureRecognizer)
self.presentationController = presentationController
return presentationController
}
// MARK: - Private Methods
@objc
private func handlePanGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let presentedVC = presentationController?.presentedViewController else {
return
}
switch gestureRecognizer.state {
case .changed:
let translation = gestureRecognizer.translation(in: presentedVC.view)
currentTranslationY = abs(translation.y)
presentedVC.view.alpha = config.getAlhpa(by: translation)
presentedVC.view.transform = CGAffineTransform(translationX: 0, y: translation.y)
gestureRecognizer.setTranslation(translation, in: presentedVC.view)
case .ended, .cancelled:
presentedVC.view.alpha = 1
if currentTranslationY > config.panGestureDismissThreshold {
presentedVC.dismiss(animated: true, completion: nil)
}
else {
currentTranslationY = 0
UIView.animate(withDuration: 0.1) {
presentedVC.view.transform = .identity
}
}
default:
break
}
}
}
위 코드에서 최소한의 필요한 내용은
UIViewControllerTransitioningDelegate를 구현한 Extension의 상위 다섯 메서드이다.
각 이름에서 알 수 있듯이
1. Present 때 사용할 UIViewControllerAnimatedTransitioning 반환 요청
2. Present 때 사용할 UIViewControllerInteractiveTransitioning 반환 요청
3. Dismiss 때 사용할 UIViewControllerAnimatedTransitioning 반환 요청
4. Dismiss 때 사용할 UIViewControllerInteractiveTransitioning 반환 요청
5. 이번 Transition때 Presenting/Presented된 뷰 컨트롤러를 관리할 UIPresentationController이다.
나는 Present할 때와 Dismiss할 때의 애니메이션 구현을 분리하고 싶어서 별도의 객체로 아래처럼 구현하였다.
final class CustomPresentingTransitionAnimator: NSObject {
private let config: CustomTransitionConfiguration
private let referenceView: UIView
private var transitionContext: UIViewControllerContextTransitioning?
// MARK: - Initializers
init(config: CustomTransitionConfiguration, referenceView: UIView) {
self.config = config
self.referenceView = referenceView
}
}
// MARK: - Extension - UIViewControllerAnimatedTransitioning
extension CustomPresentingTransitionAnimator: UIViewControllerAnimatedTransitioning {
// 트랜지션이 일어날 시간을 반환한다. 애니메이션의 총 시간을 생각하면 된다.
func transitionDuration(
using transitionContext: UIViewControllerContextTransitioning?
) -> TimeInterval {
print("\(#function)")
return config.transitionDuration
}
// transitionContext의 정보를 통해 필요한 애니메이션을적용시키면 된다.
func animateTransition(
using transitionContext: UIViewControllerContextTransitioning
) {
print("\(#function)")
guard let presentedViewController = transitionContext.viewController(forKey: .to) else {
return
}
self.transitionContext = transitionContext
let containerView = transitionContext.containerView
let backgroundView = UIView(frame: containerView.bounds)
backgroundView.backgroundColor = .white
backgroundView.alpha = 0
containerView.addSubview(backgroundView)
let transitionViewStartFrame = containerView.convert(
referenceView.bounds,
from: referenceView
)
let transitionView = UIView(frame: transitionViewStartFrame)
transitionView.backgroundColor = .clear
containerView.addSubview(transitionView)
let transitionTargetFrame: CGRect
var targetColor: UIColor? = .white
if let singleColorVC = presentedViewController as? SingleColorViewController,
let singleColorView = singleColorVC.getSingleColorView() {
transitionTargetFrame = singleColorView.frame
targetColor = singleColorView.backgroundColor
}
else {
transitionTargetFrame = containerView.bounds
}
presentedViewController.view.isHidden = true
containerView.addSubview(presentedViewController.view)
UIView.animate(
withDuration: config.transitionDuration,
delay: 0,
usingSpringWithDamping: config.springWithDamping,
initialSpringVelocity: config.initialSpringVelocity,
options: [.beginFromCurrentState]
) {
backgroundView.alpha = 1
transitionView.frame = transitionTargetFrame
transitionView.backgroundColor = targetColor
} completion: { _ in
backgroundView.removeFromSuperview()
transitionView.removeFromSuperview()
presentedViewController.view.backgroundColor = .white
presentedViewController.view.isHidden = false
transitionContext.completeTransition(transitionContext.transitionWasCancelled == false)
}
}
// 트랜지션 애니메이션의 종료 여부를 알려준다.
func animationEnded(_ transitionCompleted: Bool) {
print("\(#function)")
transitionContext = nil
}
}
final class CustomDismissalTransitionAnimator: NSObject {
private let config: CustomTransitionConfiguration
private let referenceView: UIView
private var transitionContext: UIViewControllerContextTransitioning?
// MARK: - Initializers
init(config: CustomTransitionConfiguration, referenceView: UIView) {
self.config = config
self.referenceView = referenceView
}
}
// MARK: - Extension - UIViewControllerAnimatedTransitioning
extension CustomDismissalTransitionAnimator: UIViewControllerAnimatedTransitioning {
// 트랜지션이 일어날 시간을 반환한다. 애니메이션의 총 시간을 생각하면 된다.
func transitionDuration(
using transitionContext: UIViewControllerContextTransitioning?
) -> TimeInterval {
print("\(#function)")
return config.transitionDuration
}
// transitionContext의 정보를 통해 필요한 애니메이션을적용시키면 된다.
func animateTransition(
using transitionContext: UIViewControllerContextTransitioning
) {
print("\(#function)")
// InteractiveTransitioning을 사용하므로, 아무동작도 하지 않는다.
}
// 트랜지션 애니메이션의 종료 여부를 알려준다.
func animationEnded(_ transitionCompleted: Bool) {
print("\(#function)")
transitionContext = nil
}
}
// MARK: - Extension - UIViewControllerInteractiveTransitioning
extension CustomDismissalTransitionAnimator: UIViewControllerInteractiveTransitioning {
func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) {
print("\(#function)")
guard let fromViewController = transitionContext.viewController(forKey: .from),
let singleColorVC = fromViewController as? SingleColorViewController,
let colorView = singleColorVC.getSingleColorView() else {
return
}
self.transitionContext = transitionContext
singleColorVC.view.isHidden = true
let containerView = transitionContext.containerView
let startPoint = CGPoint(x: colorView.frame.minX,
y: colorView.frame.minY + singleColorVC.view.frame.minY)
let startSize = colorView.frame.size
let transitionView = UIView(frame: CGRect(origin: startPoint, size: startSize))
transitionView.backgroundColor = colorView.backgroundColor
containerView.addSubview(transitionView)
let finalFrame = containerView.convert(referenceView.bounds, from: referenceView)
UIView.animate(withDuration: config.transitionDuration,
delay: 0,
usingSpringWithDamping: config.springWithDamping,
initialSpringVelocity: config.initialSpringVelocity,
options: [.beginFromCurrentState]) {
transitionView.frame = finalFrame
} completion: { wasCancelled in
transitionContext.completeTransition(true)
}
}
}
작업을 하면서 가장 막혔던 부분은
UIViewControllerContextTransitioning의 containerView 였다.
난 containerView위에 add된 뷰들은 애니메이션이 종료됨에 따라 UIKit에서 자연스럽게 제거하고
present하려 했던 뷰 컨트롤러가 보이는 형태인 줄 알았으나,
사실 모든건 UItransitionView를 기준으로 올라가는 것 뿐이었고
중간에 에니메이팅을 하기 위한 뷰 로직 작업도,
완료된 이후에 뷰 컨트롤러를 보일 작업도 모두다 animate시점에 이뤄져야하는 것이었다.
정리하면
containerView위에서 트랜지션을 통해 시작부터 뷰 컨트롤러가 Presented되어 사용자에게 나와서
인터렉션이 가능한 시점까지 모두 컨트롤해줘야 하는 것 이었다.
그래서 코드에 animateTransition이나 startInteractiveTransition 메서드 구현부를 보면
containerView를 기준으로 트랜지션동안 보여질 뷰들을 구현한 것 부터해서
애니메이션 완료후, 임시로 올려진 뷰들제거 및 최종 뷰 컨트롤러 설정까지 로직에 들어갔다.
이렇게 진행하면 라이프싸이클은 아래와 같다.
// present 시작
presentationController(forPresented:presenting:source:)
SingleColorViewController viewDidLoad()
animationController(forPresented:presenting:source:)
interactionControllerForPresentation(using:)
transitionDuration(using:)
SingleColorViewController viewWillAppear(_:)
transitionDuration(using:)
animateTransition(using:)
SingleColorViewController viewDidAppear(_:)
animationEnded(_:)
// present 종료
// dismiss 시작
animationController(forDismissed:)
interactionControllerForDismissal(using:)
transitionDuration(using:)
SingleColorViewController viewWillDisappear(_:)
transitionDuration(using:)
startInteractiveTransition(_:)
SingleColorViewController viewDidDisappear(_:)
animationEnded(_:)
// dismiss 종료
'스위프트' 카테고리의 다른 글
[Swift] UICollectionViewDrag & DropDelegate (0) | 2022.01.17 |
---|---|
[CoreGraphics] CGContext를 통해 뷰를 커스터마이징하게 그려보자 (0) | 2022.01.17 |
[Swift] UIViewControllerTransitioningDelegate를 사용하여 Transition을 통해 뷰 컨트롤러를 Present/Dismiss 해보자 - 1편 (0) | 2021.12.29 |
[Swift/디자인패턴] State 패턴 (0) | 2021.12.20 |
[Swift/디자인패턴] Strategy 패턴 (0) | 2021.12.20 |