This whole transition thing, it’s as simple as it gets, Can pass presentViewController: animated: completion: and dismissViewControllerAnimated: completion: a set of functions in the form of modal view, hidden view. If use the navigationController, also can call pushViewController: animated: and popViewController this a set of functions will be the new view controller, flare stack.

All of the transitions shown below are custom animations, and these effects are difficult or impossible to achieve without custom animations:

Because of the screen recording, some of the effects are not fully visible, such as the fact that it supports landscape.

Custom transitions can be complicated to implement, and all kinds of hidden bugs can be created by simply copying the code that works without understanding how it works. This article introduces the following knowledge from the shallow to the deep:

  1. Traditional closures based implementations and their drawbacks
  2. Custom present transitions
  3. Interactive animation
  4. The coordinator and UIModalPresentationCustom
  5. UINavigationController Transition animation

I created a demo for this tutorial, which you can clone on github: CustomTransition. If you find it helpful, please give a star to support it. This article is implemented with Swift+ pure code, the corresponding OC+Storyboard version can also be found in the demo, which is apple’s official demonstration code, accuracy is more guaranteed. As CocoaPods is used in demo, you may need to run the pod install command and open the.xcworkspace file.

Before you can start the tutorial, you need to download the demo. The text is pale in front of the code, and the demo contains comments that will explain everything in this article. Second, you need some background.

From and To

FromView and toView often appear in code and text. If you misunderstand what they mean, the animation logic will be completely wrong. FromView represents the current view, and toView represents the view to jump to. If view controller A was presented to B, then A is from and B is to. When we go from B view controller dismiss to A, B becomes from, and A is to. Here is a graph:

Presented and Presenting

This is also a set of relative concepts that can easily be confused with fromView and toView. In simple terms, it is not affected by the present or dismiss, if from A view controller is present to B, then A is B presentingViewController, B is always A presentedViewController.

modalPresentationStyle

This is an enumeration type that represents the type of the present animation. There are only two animations that you can customize: FullScreen and Custom. The difference between the two is that FullScreen removes fromView while Custom does not. For example, in the GIF at the beginning of this article, the third animation effect is Custom.

Block-based animation

The simplest animated transitions is transitionFromViewController method of use:

Although this method has been out of date, the analysis of it helps to understand the following knowledge. It has six parameters. The first two parameters indicate which VC to start from and which VC to jump to, and the middle two parameters indicate the time and options of the animation. The last two parameters represent the concrete implementation details of the animation and the callback closure.

These six parameters are essentially the six elements required for a transition. They can be divided into two groups. The first two parameters are a group, indicating the jump relationship of the page, and the last four are a group, indicating the execution logic of the animation.

One of the downsides of this approach is that it’s not very customizable (you’ll see later that you can customize more than just the animation), and another is that it’s not very reusable, or it’s very coupled.

Of the last two closure parameters, it is expected that both the fromViewController and toViewController parameters will be used, and they are key to the animation. Assumes that the view controller can jump to B, C, D, E, F, and A jump animation basic similar, you’ll find transitionFromViewController method to be copied many times, each time will only modify A small amount of content.

Custom present transitions

For the sake of decoupling and increasing customizability, let’s learn how to use proper poses for transitions.

First of all to understand a key concept: animated transitions agent, it is an implementation of a UIViewControllerTransitioningDelegate the object of the agreement. We need to implement this object ourselves, which will provide UIKit with one or more of the following objects:

  1. Animator:

It is to realize the UIViewControllerAnimatedTransitioning object of the agreement, is used to control the duration of the cartoon and animation display logic, agent can provide Animator for the present and dismiss process respectively, also can provide the same Animator.

  1. Interactive Animator: Similar to Animator, but interactive, more on that later
  2. Presentation Controller:

It allows for more thorough customization of the present process, such as changing the size of the presented view, adding custom views, etc., as described in more detail later.

In this section, we begin with the simplest Animator. Look back at the six essential elements of transition animation, which are divided into two groups and have no relation to each other. The function of the Animator is equivalent to the four elements of the second group, that is, for the same Animator, it can be applied to A jump to B, or it can be applied to A jump to C. It represents a generic animation logic for page hops that is not limited to specific view controllers.

If you understand this, the entire custom transition logic is clear. Take view controller A jumping to B as an example:

  1. Create an animation agent so that when things are simple, A can act as an agent
  2. Set B’s transitioningDelegate to the proxy object created in Step 1
  3. callpresentViewController:animated:completion:And set the animated parameter to true
  4. The system finds the Animator provided in the agent, and the Animator takes care of the animation logic

To explain with concrete examples:

// This class is equivalent to A
class CrossDissolveFirstViewController: UIViewController.UIViewControllerTransitioningDelegate {
// This object is equivalent to B
crossDissolveSecondViewController.transitioningDelegate = self

// Click the button to trigger the function
func animationButtonDidClicked(a) {
self.presentViewController(crossDissolveSecondViewController,
animated: true, completion: nil)}/ / the following two functions defined in UIViewControllerTransitioningDelegate agreement
// To provide animators for present and dismiss
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// You can also use the CrossDissolveAnimator. The animation effects are different
// return CrossDissolveAnimator()
return HalfWaySpringAnimator()}func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CrossDissolveAnimator()}}Copy the code

Animation of the key is how to implement animator, it implements the UIViewControllerAnimatedTransitioning agreement, at least need to implement two methods, I suggest you read carefully the comments in animateTransition method, it is the core of the whole animation logic:

class HalfWaySpringAnimator: NSObject.UIViewControllerAnimatedTransitioning {
/// set the duration of the animation
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 2
}

/// sets how the animation will proceed, with detailed comments. This method is not explained elsewhere in the demo
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
let containerView = transitionContext.containerView()

// We need to pay attention to the relationship between from/to and presented/presenting
// For a Presentation:
// fromView = The presenting view.
// toView = The presented view.
// For a Dismissal:
// fromView = The presented view.
// toView = The presenting view.

varfromView = fromViewController? .viewvartoView = toViewController? .view// iOS8 introduced the viewForKey method. Use this method whenever possible instead of directly accessing the Controller's View property
// For example, in the Form Sheet style, we add a shadow or other decoration to the presentedViewController's view. The animator will do a decoration to the entire view
PresentedViewController's view is just a child of the Decoration View
if transitionContext.respondsToSelector(Selector("viewForKey:")) {
fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
toView = transitionContext.viewForKey(UITransitionContextToViewKey)}// Let's make origin. Y in the middle of the screen so that it bounces from the middle of the screen instead of the bottom, and becomes opaque as it bouncestoView? .frame =CGRectMake(fromView! .frame.origin.x, fromView! .frame.maxY /2, fromView! .frame.width, fromView! .frame.height) toView?.alpha =0.0

// In present and, dismiss, you must add toViews to the view hierarchycontainerView? .addSubview(toView!)let transitionDuration = self.transitionDuration(transitionContext)
// Make sure to call the completeTransition method when the animation is over
UIView.animateWithDuration(transitionDuration, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, options: .CurveLinear, animations: { () -> Void intoView! .alpha =1.0     // Gradually become opaquetoView? .frame = transitionContext.finalFrameForViewController(toViewController!)// Move to the specified location
}) { (finished: Bool) - >Void in
letwasCancelled = transitionContext.transitionWasCancelled() transitionContext.completeTransition(! wasCancelled) } } }Copy the code

The core of the animateTransition method is to get the necessary information from the transition animation context to complete the animation. The context is an implementation of a UIViewControllerContextTransitioning object, its role is to provide essential information for animateTransition method. You should not cache any information about the animation, but should always get it from the context of the transition animation (such as fromView and toView) to ensure that the information is always up to date and correct.

After get enough information, we call UIView. AnimateWithDuration methods give the Core Animation Animation processing. Don’t forget to execute the completeTransition method after the animation call ends.

This section is detailed in code in the Cross Dissolve folder of Demo. There are two animator files, which means we can provide the same animator for present and Dismiss, or we can provide their respective animators separately. You can use the same animator if the animations are similar, with the only differences:

  1. Present, you need totoViewAdds to the view hierarchy of Container.
  2. In dismiss, I want tofromViewRemoves container from the view hierarchy.

If you’re confused by all this code and knowledge, or if you don’t need it for a while, you should at least remember the basics and flow of custom animation:

  1. Set the view controller to jump to (presentedViewController)transitioningDelegate
  2. The object acting as a proxy can be the source view controller (presentingViewController), or a self-created object, which needs to provide an animator object for the transition.
  3. Animator objectsanimateTransitionIs the core logic of the whole animation.

Interactive animation

When the toViewController’s transitioningDelegate property is set and presented, UIKit will fetch the animator from the delegate. Here’s another detail: UIKit will call agent interactionControllerForPresentation: method to get the interactive controller, if be nil is performing the interactive animation, it is returned to the content of the previous section.

If the object is not nil, UIKit does not call the animateTransition method of the animator, but instead calls the interactive controller (remember the animation proxy schematic from earlier, Interactive animation controller and the animator is flat level relations) startInteractiveTransition: method.

So-called interactive animations, which are usually gesture-driven, produce a percentage of the animation completed to control the animation effect (the second animation effect in the GIF at the beginning of this article). The entire animation is no longer a one-time, coherent completion, but can change the percentage or even cancel at any time. This requires an implementation of a UIPercentDrivenInteractiveTransition agreement interactive animation controller and the animator to work together. This may seem like a very complex task, but UIKit encapsulates enough detail that we just need to define a time handler (such as a swiping gesture) in the interactive animation controller and then when a new event is received, Calculate the percentage of the animation to complete and invoke updateInteractiveTransition schedule to update the animation.

The following code briefly shows the process (with some details and comments removed, please do not use this as a correct reference). For the complete code, see the Interactivity folder in demo:

// This is equivalent to fromViewController
class InteractivityFirstViewController: UIViewController {
// This is equivalent to toViewController
lazy var interactivitySecondViewController: InteractivitySecondViewController = InteractivitySecondViewController(a)/ / defines a InteractivityTransitionDelegate class as an agent
lazy var customTransitionDelegate: InteractivityTransitionDelegate = InteractivityTransitionDelegate(a)override func viewDidLoad(a) {
super.viewDidLoad()
setupView() // It is mainly the layout of some UI controls, and the implementation details can be ignored

/// set the animation agent. This agent is complicated, so we create a new agent object instead of self as the agent
interactivitySecondViewController.transitioningDelegate = customTransitionDelegate
}

/ / trigger gesture, also call animationButtonDidClicked method
func interactiveTransitionRecognizerAction(sender: UIScreenEdgePanGestureRecognizer) {
if sender.state == .Began {
self.animationButtonDidClicked(sender)
}
}

func animationButtonDidClicked(sender: AnyObject) {
self.presentViewController(interactivitySecondViewController, animated: true, completion: nil)}}Copy the code

The non-interactive animation agent only needs to provide an animator for present and dismiss, but in the interactive animation agent, it also needs to provide an interactive animation controller for present and dismiss:

class InteractivityTransitionDelegate: NSObject.UIViewControllerTransitioningDelegate {
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return InteractivityTransitionAnimator(targetEdge: targetEdge)
}

func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return InteractivityTransitionAnimator(targetEdge: targetEdge)
}

// The first two functions are the same as in the fade in/fade out demo
/// The latter two functions are used for interactive animation

func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return TransitionInteractionController(gestureRecognizer: gestureRecognizer, edgeForDragging: targetEdge)
}

func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return TransitionInteractionController(gestureRecognizer: gestureRecognizer, edgeForDragging: targetEdge)
}
}
Copy the code

Omit the code in the animator, which is similar to the animator in non-interactive animation. Since interactive animations are just the icing on the cake, it must support non-interactive animations, such as in this case, a button click is still a non-interactive animation, but only a gesture swipe will trigger an interactive animation.

class TransitionInteractionController: UIPercentDrivenInteractiveTransition {
/// Fires this function when the gesture is swiped
func gestureRecognizeDidUpdate(gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
switch gestureRecognizer.state {
case .Began: break
case .Changed: self.updateInteractiveTransition(self.percentForGesture(gestureRecognizer))  // Gesture swipe to update the percentage
case .Ended:    // Finish the slide, determine if it is more than half, if it is, complete the rest of the animation, otherwise cancel the animation
if self.percentForGesture(gestureRecognizer) >= 0.5 {
self.finishInteractiveTransition()
}
else {
self.cancelInteractiveTransition()
}
default: self.cancelInteractiveTransition()
}
}
private func percentForGesture(gesture: UIScreenEdgePanGestureRecognizer) -> CGFloat {
letPercent = calculated by gesturereturn percent
}
}
Copy the code

Interactive animation is implemented on the basis of the interactive animation, we need to create an inherited from UIPercentDrivenInteractiveTransition types of subclasses, and returns the instance of the type of object in the animation agent.

In this type, listen for changes in the timing of the gesture (or download progress, etc.) and then call the percentForGesture method to update the progress of the animation.

The coordinator and UIModalPresentationCustom

While transitioning, you can also do some synchronized, additional animations, such as the third example in the GIF at the beginning of this article. Presentedviews and PresentingViews can change their view hierarchy, adding additional effects (shadows, rounded corners). UIKit uses the Transition coordinator to manage these additional animations. You can retrieve the transitionCoordinator from the transitionCoordinator property of the view controller where the animation is to be generated. The transitionCoordinator exists only during the execution of the transition animation.

Want to achieve the effects of a third example in GIF, we also need to use UIModalPresentationStyle. Custom instead. FullScreen. Because the latter removes the fromViewController, which is clearly not what you need.

When present mode is. When Custom, we can also use UIPresentationController to more thoroughly control the effect of the transition animation. A Presentation Controller has the following functions:

  1. Set up thepresentedViewControllerView size of
  2. Add custom views to changepresentedViewThe appearance of the
  3. Provides transition animation effects for any custom view
  4. Reactive layout based on size class

You can argue that.fullscreen and other present styles that Swift provides for our implementation are. Special case of Custom. Custom allows usto define transitions more freely.

UIPresentationController provides four functions to define what to do before and after the present and Dismiss animations begin:

  1. presentationTransitionWillBegin: present is about to be executed
  2. presentationTransitionDidEnd: present After the execution is complete
  3. dismissalTransitionWillBegin: When the dismiss is about to be executed
  4. dismissalTransitionDidEnd: After the dismiss execution is complete

The following code briefly describes the implementation of the third animation effect in the GIF. You can view the completion code under the Demo’s Custom Presentation folder:

// This is equivalent to fromViewController
class CustomPresentationFirstViewController: UIViewController {
// This is equivalent to toViewController
lazy var customPresentationSecondViewController: CustomPresentationSecondViewController = CustomPresentationSecondViewController(a)/ / create PresentationController
lazy var customPresentationController: CustomPresentationController = CustomPresentationController(presentedViewController: self.customPresentationSecondViewController, presentingViewController: self)

override func viewDidLoad(a) {
super.viewDidLoad()
setupView() // It is mainly the layout of some UI controls, and the implementation details can be ignored

// Set the transition animation agent
customPresentationSecondViewController.transitioningDelegate = customPresentationController
}

override func didReceiveMemoryWarning(a) {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}

func animationButtonDidClicked(a) {
self.presentViewController(customPresentationSecondViewController, animated: true, completion: nil)}}Copy the code

The emphasis is on how to implement CustomPresentationController this class:

class CustomPresentationController: UIPresentationController.UIViewControllerTransitioningDelegate {
var presentationWrappingView: UIView?  // This view encapsulates the original view, adding shadows and rounded corners
var dimmingView: UIView? = nil  // Black mask with alpha 0.5

// Tell UIKit which view to animate
override func presentedView(a) -> UIView? {
return self.presentationWrappingView
}
}

// The four methods customize the actions before and after the transition
extension CustomPresentationController {
override func presentationTransitionWillBegin(a) {
// Set the presentationWrappingView and dimmingView UI effects
let transitionCoordinator = self.presentingViewController.transitionCoordinator()
self.dimmingView? .alpha =0
// Perform the synchronized animation through the transition coordinatortransitionCoordinator? .animateAlongsideTransition({ (context:UIViewControllerTransitionCoordinatorContext) - >Void in
self.dimmingView? .alpha =0.5
}, completion: nil)}/// At the end of the present, clear both dimmingView and wrappingView. These temporary views are no longer needed
override func presentationTransitionDidEnd(completed: Bool) {
if! completed {self.presentationWrappingView = nil
self.dimmingView = nil}}/// When dismiss starts, make the dimmingView completely transparent. This animation happens at the same time as the animation in the animator
override func dismissalTransitionWillBegin(a) {
let transitionCoordinator = self.presentingViewController.transitionCoordinator() transitionCoordinator? .animateAlongsideTransition({ (context:UIViewControllerTransitionCoordinatorContext) - >Void in
self.dimmingView? .alpha =0
}, completion: nil)}// After work, empty the dimmingView and wrappingView. These temporary views are no longer needed
override func dismissalTransitionDidEnd(completed: Bool) {
if completed {
self.presentationWrappingView = nil
self.dimmingView = nil}}}extension CustomPresentationController {}Copy the code

In addition, this class handles logic related to the layout of the child views. As an animation agent, it also needs to provide an animator object for the animation. Please read the detailed code in demo’s Custom Presentation folder.

UINavigationController Transition animation

So far, all transitions have been for present and dismiss, but UINavigationController can also customize transitions. The two are parallel, and many can be compared:

class FromViewController: UIViewController.UINavigationControllerDelegate {
let toViewController: ToViewController = ToViewController(a)override func viewDidLoad(a) {
super.viewDidLoad()
setupView() // It is mainly the layout of some UI controls, and the implementation details can be ignored

self.navigationController.delegate = self}}Copy the code

Unlike the present/dismiss, view controller to realize now is UINavigationControllerDelegate agreement, let oneself become navigationController agent. This agreement is similar to the previous UIViewControllerTransitioningDelegate agreement.

FromViewController implementation UINavigationControllerDelegate protocol specific operation is as follows:

func navigationController(navigationController: UINavigationController,
animationControllerForOperation operation: UINavigationControllerOperation,
fromViewController fromVC: UIViewController,
toViewController toVC: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
if operation == .Push {
return PushAnimator()}if operation == .Pop {
return PopAnimator()}return nil;
}
Copy the code

As for the animator, there is no difference at all. As you can see, a well-packaged animator can be used not only in present/dismiss, but even in push/ POP.

The UINavigationController can also add interactive transitions in a similar way.

conclusion

For non-interactive animations, you need to set the transitioningDelegate property of the presentedViewController, which needs to provide animators for present and dismiss. The duration and presentation logic of the animation are specified in the Animator.

For interactive animations, the transitioningDelegate property provides an interactive animation controller on top of the previous one. Do event processing in the controller, and then update the animation completion progress.

For custom animations, you can use the four functions in UIPresentationController to customize the effects of the animation before and after execution. You can change the size and appearance of the presentedViewController, and synchronize other animations.

The water of custom animation is still relatively deep, this article is only suitable for the introduction of learning, welcome to communicate with each other.