Creating Custom Presentations

📌 Custom Transition 에 관한 배경 지식이 필요한 글입니다.

Presentation Controller

  • presented VC 가 present 되고 dismiss 될 때까지 UIKit이 reference를 유지
  • 역할
    • present, dismiss 에 대응

      1. presented VC 의 사이즈 설정
      2. presented content 의 시각적 모습을 바꾸기 위해 custom view 추가 (ex. dimmingView)
      3. custom view 에 transition animation 제공
        (Animator 객체의 transition animation 과 동기화 가능)
    • 앱 환경이 변경 될 때 presentation 의 시각적 모양 조정
      ex) size class 가 compact 에서 regular 로 변경, rotation이 발생

1. Custom Presentation 사용하기

  1. VC 의 modalPresentationStyle 을 custom 으로 설정
  2. VC 의 transitioningDelegate 설정
@IBAction func menuButtonTapped(_ sender: Any) {
    let secondVC = storyboard!.instantiateViewController(identifier: "SideVC")
        
    secondVC.modalPresentationStyle = .custom
    secondVC.transitioningDelegate = self
        
    present(secondVC, animated: true)
}

2. Custom Presentation 절차

presentation controller 는 animator 객체와 함께 작동하여 전반적인 transition 구현

custom transition 을 구현할 때와 마찬가지로
transitioningDelegate를 통해 presentationController 객체를 UIKit 에게 제공

presentation controller, Animator 객체 모두 animation 을 구현할 수 있는 메서드가 제공됩니다.
주요한 차이점은 presentation controller 의 경우 present, dismiss 에 따라 호출되는 메서드가 분리돼서 제공되지만, Animator 객체의 경우 animate Transition 만 제공됩니다. 따라서 Animator 객체present 용도, dismiss 용도로 객체를 2개를 만들거나 혹은 하나의 객체에서 present, dismiss 를 분기처리하여 구현할 수 있습니다.

제가 만든 예제에서는 분기처리 하는 방식으로 구현하긴 했지만, 객체를 나눠서 구현하는게 더 가독성이 좋아 보이고 애니메이션이 복잡해질 수록 더 효율적일 것 같습니다.

  1. custom presentation controller 를 얻기 위해 transitioning delegate 의 presentationController(forPresented:presenting:source:) 를 호출
  2. transitioning delegate 에게 animator, interactive animator 객체를 요청
  3. presentation controller 의 presentationTransitionWillBegin() 을 호출
    -> 여기서 custom view 를 view hierarchy 에 추가하고 해당 view 에 대한 animation 을 구성해야 한다.
  4. Animator 객체에서 transition animation 을 수행
  5. presentationTransitionDidEnd(_:) 호출
    -> presentation animation 이 끝나면 presentation 에게 알림

3. Presentation Controller 의 프로퍼티 및 메서드

3-1. Presented VC 의 frame 설정

  • frameOfPresentedViewInContainerView

    presented VC 는 기본적으로 container view 의 frame 을 꽉 채우지만,
    frameOfPresentedViewInContainerView 프로퍼티를 이용하여 frame 을 설정함으로써
    화면의 일부만 채울 수 있다.

    animator 객체는 frameOfPresentedViewInContainerView 에 의해 반환된 frame 값으로 presented VC 를 애니메이션 하는 역할을 한다.

    frameOfPresentedViewInContainerView 로 설정한 PresentedView 의 frame 값은 Animator 객체의 animateTransition 메서드 안에서 finalFrame(forKey: toVC) 을 통해 값을 얻고 활용할 수 있다.

ex) container view 의 절반만 덮도록 frame 값을 변경하는 예시

override var frameOfPresentedViewInContainerView: CGRect {
        var presentedViewFrame: CGRect = .zero
        let containerBounds = containerView!.bounds
        
        presentedViewFrame.size = CGSize(width: containerBounds.width / 2,
                                         height: containerBounds.height)
        
        return presentedViewFrame
}

3-2. Custom views 관리 및 애니메이션

presentation controller 는 presentation 과 관련된 모든 custom view 들을 생성하고 관리하는 책임을 가짐

  • init

    일반적으로 init 에서 custom view 들을 생성한다.

ex) presentation controller 의 init 에서 gesture recognizer 를 추가한 dimming view 를 만드는 예제

override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
     dimmingView = UIView()
        
     super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
        
     dimmingView.translatesAutoresizingMaskIntoConstraints = false
     dimmingView.backgroundColor = UIColor(white: 0, alpha: 0.1)
     dimmingView.alpha = 0
     
     let recognizer = UITapGestureRecognizer(target: self, action: #selector(dimmingViewTapped))
     dimmingView.addGestureRecognizer(recognizer)
}

@objc
func dimmingViewTapped() {
    presentingViewController.dismiss(animated: true)
}


  • presentationTransitionWillBegin

    custom view 들을 구성하고 container view 에 추가
    presented 혹은 presenting VC 의 transition coordinator 를 이용하여 애니메이션을 생성

override func presentationTransitionWillBegin() {
     let containerViewBounds = containerView!.bounds

     dimmingView.frame = containerViewBounds

     containerView?.insertSubview(dimmingView, at: 0)

     guard let coordinator = presentedViewController.transitionCoordinator else {
         dimmingView.alpha = 1
         return
     }
        
     coordinator.animate { _ in
         self.dimmingView.alpha = 1
     }
 }


  • animate(alongsideTransition:completion:)

animator 객체에서 처리되지 않는 애니메이션을 처리하고자 할 때 사용

animate alongside 의 애니메이션 블럭 내의 코드animator 객체의 animateTransition 메서드 내에서 UIView.animate 와 동시에 수행 (즉, transition animation 과 동시에 수행된다는 뜻)
하지만 Core Animation 으로 animate 하는 경우에는 animateTransition 메서드가 리턴되고 나서 수행된다.

  • 정리하자면,
    presentation controller 에서 추가한 custom view 에 대해 animation을 추가하고자 할 때, animator 객체의 transition animation 과 동기화시켜야 할 것입니다. 그럴 때 사용할 수 있는 메서드입니다.
    transition coordinator 에서도 동일하게 기술하고 있습니다.


  • containerView

transition 과정에서 사용되는 view 들의 superView 로써,
containerview 는 presentation controller, animator 객체 둘 다 공용으로 사용

(애니메이션 되는 view 들은 모두 container view의 subview 여야 합니다. 때문에 위 예제에서도 presentation controller 에서 dimming view 를 animate 하기 전 container view 의 subView 로 만들어줍니다)



  • presentationTransitionDidEnd

presentation 이 끝나면 presentationTransitionDidEnd 를 이용하여 presentation 취소로 인한 정리(clean up)를 처리한다.

override func presentationTransitionDidEnd(_ completed: Bool) {
    if !completed {
      dimmingView.removeFromSuperview()
    }
}


  • dismissalTransitionWillBegin

    dismiss 때 호출되는 메서드.

override func dismissalTransitionWillBegin() {
    guard let coordinator = presentedViewController.transitionCoordinator else {
      dimmingView.alpha = 0.0
      return
    }
    
    coordinator.animate(alongsideTransition: { _ in
      self.dimmingView.alpha = 0.0
    })
}


  • dismissalTransitionDidEnd
override func dismissalTransitionDidEnd(_ completed: Bool) {
    if completed {
      dimmingView.removeFromSuperview()
    }
}
  • Custom Presentation 만 적용 시

  • Custom Presentation + Custom Transition

소스코드:
https://github.com/tksrl0379/CustomTransition-Presentation

아래는 Custom Presentation & Transition 의 핵심 객체인
Transition Delegate, Presentation Controller, Animator 의 코드입니다.
전체적인 흐름은 주요 메서드들에 표시한 숫자의 순서에 따라 진행됩니다.

소스코드 전문을 보고 싶으시면 위 링크를 참고해주세요.

Transition Delegate (Presentation controller, Animator 제공)

extension MainVC: UIViewControllerTransitioningDelegate {
    
    // MARK: Custom Presentation ( Presentation Controller )
    
    // 1
    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        CustomPresentationController(presentedViewController: presented, presenting: presenting)
    }
    
    
    // MARK: Custom Transition ( Animator Object )
    
    // 2
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CustomAnimator(isPresenting: true)
    }

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CustomAnimator(isPresenting: false, interactionController: (dismissed as? SideVC)?.swipeInteractionController)
    }
}

Presentation Controller (Presentation 관리)

class CustomPresentationController: UIPresentationController {
    
    private var dimmingView: UIView
    
    override var frameOfPresentedViewInContainerView: CGRect {
        var presentedViewFrame: CGRect = .zero
        let containerBounds = containerView!.bounds
        
        presentedViewFrame.size = CGSize(width: containerBounds.width * 0.8,
                                         height: containerBounds.height)
        
        return presentedViewFrame
    }
    
    override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
        dimmingView = UIView()
        
        super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
        
        dimmingView.translatesAutoresizingMaskIntoConstraints = false
        dimmingView.backgroundColor = UIColor(white: 0, alpha: 0.2)
        dimmingView.alpha = 0
        
        let recognizer = UITapGestureRecognizer(target: self, action: #selector(dimmingViewTapped))
        dimmingView.addGestureRecognizer(recognizer)
    }
    
    
    // MARK: Presentaiton
    
    // 3
    override func presentationTransitionWillBegin() {
        super.presentationTransitionWillBegin()
        
        let containerViewBounds = containerView!.bounds
        
        dimmingView.frame = containerViewBounds
        dimmingView.alpha = 0
        
        containerView?.insertSubview(dimmingView, at: 0)
        
        guard let coordinator = presentedViewController.transitionCoordinator else {
            dimmingView.alpha = 1
            return
        }
        
        
        // animator 객체의 animateTransition 메서드 내의 UIView.animate 와 동시에 수행
        coordinator.animate { _ in
            // 6
            self.dimmingView.alpha = 1
        }
    }
    
    
    override func presentationTransitionDidEnd(_ completed: Bool) {
        super.presentationTransitionDidEnd(completed)
    }

    
    
    // MARK: Dismissal
    
    override func dismissalTransitionWillBegin() {
        guard let coordinator = presentedViewController.transitionCoordinator else {
            dimmingView.alpha = 0
            return
        }
        
        
        coordinator.animate(alongsideTransition: { _ in
            self.dimmingView.alpha = 0
        })
    }
}

extension CustomPresentationController {
    @objc
    func dimmingViewTapped() {
        presentingViewController.dismiss(animated: true)
    }
}

Animator 객체 (Transition Animate 관리)

class CustomAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
    let interactionController: SwipeInteractionController?
    
    private let isPresenting: Bool
    private let duration: TimeInterval = 0.15
    
    init(isPresenting: Bool, interactionController: SwipeInteractionController? = nil) {
        self.isPresenting = isPresenting
        self.interactionController = interactionController
        
        super.init()
    }
    
    // 4
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        duration
    }
    
    // 5
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        
        let containerView = transitionContext.containerView
        
        let firstKey: UITransitionContextViewControllerKey = isPresenting ? .from : .to
        let secondKey: UITransitionContextViewControllerKey = isPresenting ? .to : .from
        
        let mainVC = transitionContext.viewController(forKey: firstKey)!
        let sideVC = transitionContext.viewController(forKey: secondKey)!
        
        if isPresenting {
            containerView.addSubview(sideVC.view)
        }
        
        let endFrame = transitionContext.finalFrame(for: sideVC)
        var startFrame = endFrame
        startFrame.origin.x -= startFrame.width

        let initialFrame = isPresenting ? startFrame : endFrame
        let finalFrame = isPresenting ? endFrame : startFrame
        
        sideVC.view.frame = initialFrame
        
        
        UIView.animate(withDuration: duration, delay: 0, options: .curveLinear) {
            // 6
            sideVC.view.frame = finalFrame
            mainVC.view.frame.origin.x += (self.isPresenting ? finalFrame.width : -finalFrame.width)
        } completion: { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }
    }
}

- 참고 문서

View Controller Programming Guide for iOS
https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/DefiningCustomPresentations.html#//apple_ref/doc/uid/TP40007457-CH25-SW1

UIPresentationController Tutorial: Getting Started
https://www.raywenderlich.com/3636807-uipresentationcontroller-tutorial-getting-started

좋은 웹페이지 즐겨찾기