自定义交互过渡动画

custom interactive transition animation

我想实现两个视图控制器之间的交互转换。我希望它是模态或现在的过渡。

我知道我需要使用以下内容。

transitioningDelegate

animationController(forPresented:presenting:Source:)

interactionControllerForPresentation(Using:)

UIPercentDrivenInteractiveTransition

我不知道如何实现这一切。我似乎在 swift 中找不到任何有用的东西或任何工作示例 3. 现在我创建了一个简单的单视图应用程序,它有两个视图控制器 VC1(蓝色背景)和 VC2(黄色背景)来轻松测试任何可能的解决方案。

请参阅 WWDC 2013 视频 Custom Transitions Using View Controllers for discussion of the transition delegate, animation controller, and interaction controller. See WWDC 2014 videos View Controller Advancements in iOS 8 and A Look Inside Presentation Controllers 了解演示控制器(您也应该使用)。

基本思想是创建一个转换委托对象,该对象标识将用于自定义转换的动画控制器、交互控制器和演示控制器:

class TransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {

    /// Interaction controller
    ///
    /// If gesture triggers transition, it will set will manage its own
    /// `UIPercentDrivenInteractiveTransition`, but it must set this
    /// reference to that interaction controller here, so that this
    /// knows whether it's interactive or not.

    weak var interactionController: UIPercentDrivenInteractiveTransition?

    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return PullDownAnimationController(transitionType: .presenting)
    }

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return PullDownAnimationController(transitionType: .dismissing)
    }

    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        return PresentationController(presentedViewController: presented, presenting: presenting)
    }

    func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactionController
    }

    func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactionController
    }

}

然后您只需指定正在使用的自定义转换以及应使用的转换委托。您可以在实例化目标视图控制器时执行此操作,或者您可以将其指定为目标视图控制器的 init 的一部分,例如:

class SecondViewController: UIViewController {

    let customTransitionDelegate = TransitioningDelegate()

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        modalPresentationStyle = .custom
        transitioningDelegate = customTransitionDelegate
    }

    ...
}

动画控制器指定动画的详细信息(如何设置动画、non-interactive 转换所用的持续时间等):

class PullDownAnimationController: NSObject, UIViewControllerAnimatedTransitioning {

    enum TransitionType {
        case presenting
        case dismissing
    }

    let transitionType: TransitionType

    init(transitionType: TransitionType) {
        self.transitionType = transitionType

        super.init()
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let inView   = transitionContext.containerView
        let toView   = transitionContext.view(forKey: .to)!
        let fromView = transitionContext.view(forKey: .from)!

        var frame = inView.bounds

        switch transitionType {
        case .presenting:
            frame.origin.y = -frame.size.height
            toView.frame = frame

            inView.addSubview(toView)
            UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
                toView.frame = inView.bounds
            }, completion: { finished in
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            })
        case .dismissing:
            toView.frame = frame
            inView.insertSubview(toView, belowSubview: fromView)

            UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
                frame.origin.y = -frame.size.height
                fromView.frame = frame
            }, completion: { finished in
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            })
        }
    }

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.5
    }
}

上面的动画控制器处理呈现和解散,但如果感觉太复杂,理论上你可以将它分成两个 classes,一个用于呈现,另一个用于解散。但我不喜欢两个不同的 class 如此紧密地耦合,所以我将承担 animateTransition 的轻微复杂性的成本,以确保它们都很好地封装在一个 class.

无论如何,我们想要的下一个对象是呈现控制器。在这种情况下,表示控制器告诉我们从视图层次结构中删除原始视图控制器的视图。 (在这种情况下,我们这样做是因为您要过渡到的场景恰好占据了整个屏幕,因此无需在视图层次结构中保留旧视图。)如果您要添加任何其他额外的 chrome(例如添加 dimming/blurring 视图等),这将属于演示控制器。

无论如何,在这种情况下,呈现控制器非常简单:

class PresentationController: UIPresentationController {
    override var shouldRemovePresentersView: Bool { return true }
}

最后,您可能想要一个手势识别器:

  • 实例化UIPercentDrivenInteractiveTransition
  • 自己启动转换;
  • 随着手势的进行更新 UIPercentDrivenInteractiveTransition
  • 在手势完成时取消或结束交互过渡;
  • 完成后删除 UIPercentDrivenInteractiveTransition(以确保它不会挥之不去,因此它不会干扰您稍后可能想做的任何 non-interactive 转换......这是一个微妙的容易被忽视的小点)。

所以 "presenting" 视图控制器可能有一个手势识别器,可以做类似的事情:

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let panDown = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
        view.addGestureRecognizer(panDown)
    }

    var interactionController: UIPercentDrivenInteractiveTransition?

    // pan down transitions to next view controller

    func handleGesture(_ gesture: UIPanGestureRecognizer) {
        let translate = gesture.translation(in: gesture.view)
        let percent   = translate.y / gesture.view!.bounds.size.height

        if gesture.state == .began {
            let controller = storyboard!.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
            interactionController = UIPercentDrivenInteractiveTransition()
            controller.customTransitionDelegate.interactionController = interactionController

            show(controller, sender: self)
        } else if gesture.state == .changed {
            interactionController?.update(percent)
        } else if gesture.state == .ended || gesture.state == .cancelled {
            let velocity = gesture.velocity(in: gesture.view)
            if (percent > 0.5 && velocity.y == 0) || velocity.y > 0 {
                interactionController?.finish()
            } else {
                interactionController?.cancel()
            }
            interactionController = nil
        }
    }

}

您可能还想更改它,使其只识别向下的手势(而不是任何旧的平底锅),但希望这能说明这个想法。

并且您可能希望 "presented" 视图控制器具有用于关闭场景的手势识别器:

class SecondViewController: UIViewController {

    let customTransitionDelegate = TransitioningDelegate()

    required init?(coder aDecoder: NSCoder) {
        // as shown above
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let panUp = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
        view.addGestureRecognizer(panUp)
    }

    // pan up transitions back to the presenting view controller

    var interactionController: UIPercentDrivenInteractiveTransition?

    func handleGesture(_ gesture: UIPanGestureRecognizer) {
        let translate = gesture.translation(in: gesture.view)
        let percent   = -translate.y / gesture.view!.bounds.size.height

        if gesture.state == .began {
            interactionController = UIPercentDrivenInteractiveTransition()
            customTransitionDelegate.interactionController = interactionController

            dismiss(animated: true)
        } else if gesture.state == .changed {
            interactionController?.update(percent)
        } else if gesture.state == .ended {
            let velocity = gesture.velocity(in: gesture.view)
            if (percent > 0.5 && velocity.y == 0) || velocity.y < 0 {
                interactionController?.finish()
            } else {
                interactionController?.cancel()
            }
            interactionController = nil
        }

    }

    @IBAction func didTapButton(_ sender: UIButton) {
        dismiss(animated: true)
    }

}

有关上述代码的演示,请参阅 https://github.com/robertmryan/SwiftCustomTransitions

看起来像:

但是,归根结底,自定义转场有点复杂,所以我再次向您推荐那些原始视频。在发布任何进一步的问题之前,请确保您仔细观察了它们。您的大部分问题可能会在这些视频中得到解答。