Watch the WWDC 2013 Custom Transitions video using view controllers for a discussion of the transition delegate, animation controller, and interaction controller. See WWDC 2014 Videos View controller enhancements in iOS 8 and View inside presentation controllers for a introduction to presentation controllers (which you should also use).
The basic idea is to create a transition delegate object that identifies which animation controller, interaction controller, and view controller will be used for the custom transition:
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 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 } }
You just need to indicate that you are using a custom transition, and that you need to use a delegate translator. You can do this when creating an instance of the destination controller, or you can specify it as part of the init
for the destination view controller, for example:
class SecondViewController: UIViewController { let customTransitionDelegate = TransitioningDelegate() required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) modalPresentationStyle = .custom transitioningDelegate = customTransitionDelegate } ... }
The animation controller sets the details of the animation (like the animation, the duration that will be used for non-interactive transitions, etc.):
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 } }
The above animation controller handles both the view and the rejection, but if it is too complicated, you can theoretically break it into two classes: one for the presentation and the other for the rejection. But I don’t like to have two different classes that are so closely related to each other, so I will bear the cost of the small complexity of animateTransition
to make sure that all this is perfectly encapsulated in one class.
In any case, the next object we want is a view controller. In this case, the view manager tells us to remove the view controller view from the view hierarchy. (We do this, in this case, because the scene you are transitioning to takes up the entire screen, so there is no need to store the old view in the hierarchy of views.) If you added another extra chrome (like adding dimming / blurring, etc.) e.) that will belong to the view controller.
In any case, the view controller is pretty simple:
class PresentationController: UIPresentationController { override var shouldRemovePresentersView: Bool { return true } }
Finally, you probably want a gesture recognizer:
- creates an instance of
UIPercentDrivenInteractiveTransition
; - initiates the transition itself;
- updates the
UIPercentDrivenInteractiveTransition
as gestures progress; - either cancels or completes the interactive transition when the gesture is done; and
- removes
UIPercentDrivenInteractiveTransition
when done (to make sure it doesn't linger, so it doesn't interfere with any non-interactive transitions that you might want to do later ... it's a thin little point that is easy to overlook).
Thus, the “view” of the view controller may have a gesture recognizer that can do something like:
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 } } }
You probably also want to change this so that it only recognizes gestures down (and not any old panoramas), but hopefully this illustrates the idea.
And you apparently want the “presented” view controller to have a gesture recognizer to reject the scene:
class SecondViewController: UIViewController { let customTransitionDelegate = TransitioningDelegate() required init?(coder aDecoder: NSCoder) {
See https://github.com/robertmryan/SwiftCustomTransitions for a demonstration of the above code.
Looks like:
But on the bottom line, custom transitions are a bit complicated, so I refer back to these original videos again. Make sure you review them carefully before submitting any additional questions. Most of your questions will most likely be answered in these videos.