In iOS development, the jump between interfaces is actually the jump of the controller. There are many kinds of jumps, the most commonly used ones are push and modal.

  • Modal: Any controller can be represented in modal form. Effect: The new controller drills up from the bottom of the screen until it covers the previous controller. The system will also have an animation.
public func presentViewController(viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?)
public func dismissViewControllerAnimated(flag: Bool, completion: (() -> Void)?)Copy the code
  • Push: In push, the management of the controller is actually handed over to the UINavigationController, so you must get the corresponding navigation controller when you push the controller. Effect: From right to left, the system will have a default animation.
public func pushViewController(viewController: UIViewController, animated: Bool) 
public func popViewControllerAnimated(animated: Bool) -> UIViewController?Copy the code

Both Push and Model have systems to provide transition animation effects, but sometimes what the system provides may not meet the development needs, which requires customization. To be honest, customization of transition animation is still a bit troublesome. But the principle is very simple.

The principle of transition animation:

When two controllers are pushed. Pop or modal.dismiss, the system will put the original controller in the controller container responsible for the transition, and the target controller will also be put in, but the target controller is not visible, so what we need to do is to expose the new controller and remove the old controller. It’s simple!

The above roughly introduced the controller switch and principle, this article we are going to talk about custom modal transfer animation, a push a modal, although custom modal transfer animation is more used, but push can also understand it! Take a look at the results and then get ready to board:




Custom modal transition animation




Custom push transition animation

1. Customize modal transition animation

1. Simple modal effect:




Very simple modal effect, first there is a master controller, the master controller has a scrollView at the bottom, add gesture listener to the scrollView picture, and in the listener method modal a controller like background picture, so you can start custom transition animation!

2. Set the agent of transition animation:

  1. I’m going to create a new inheritance NSObject, UIViewControllerAnimatedTransitioning PopAnimator file, used to set the transitions of the animation agent method, in the add UIViewControllerAnimatedTransitioning agreement must be in order to realize the two agents Methods:
    / / set the transitions animation duration func transitionDuration (transitionContext: UIViewControllerContextTransitioning?) NSTimeInterval {return 0} func animateTransition(transitionContext: UIViewControllerContextTransitioning) { }Copy the code
  2. Set the herbDetailsVc(the controller for each image) transitioningDelegate to itself in the ViewController(the primary controller), and set a constant let Transition = for the file you just created PopAnimator (), a new extension UIViewControllerTransitioningDelegate comply with the agency

    extension ViewController: UIViewControllerTransitioningDelegate {
    
    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {  
       return transition
    }
    
    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
       return nil
    }
    }Copy the code

    If it returns nil, use the system’s default transition animation.

3. Create transitions.

Set the time in the transitionDuration: method of PopAnimator. Set the content of the transition animation

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {/ / get the container let containerView = transitionContext. ContainerView ()! // Get the target view // viewForKey get the new and old controller view // viewControllerForKey get the new and old controller let toView = transitionContext.viewForKey(UITransitionContextToViewKey)! ContainerView. AddSubview (toView) toView. Alpha = 0.0 UIView. AnimateWithDuration (duration, animations: {() - > Void in toView. Alpha = 1.0}) {(_) - > Void in / / ferry animation complete transitionContext.com pleteTransition (true)}}Copy the code

The content is: by modifying the transparency, achieve a gradient effect, as shown below.




Such a simple transition effect is realized! Isn’t it easy? It’s just not what we want yet.

4. Pop-up effect in the upper left corner

Note: This step is modified from step 3. Add var presenting = true to PopAnimator. This is used to determine whether to eject the controller or back, since both forward and back invoke the animateTransition proxy method. The next implementation won’t be needed, but let’s set it up like this. Also set a var originFrame = cgrect. zero to set the frame of the destination controller. Change the original code in the animateTransition to the following

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {/ / get the container let containerView = transitionContext. ContainerView ()! // Get the target view // viewForKey get the new and old controller view // viewControllerForKey get the new and old controller let toView = transitionContext.viewForKey(UITransitionContextToViewKey)! let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)! // Get a view that needs to be animated let herbView = presenting? ToView: fromView // Get the initial and final frame let initialFrame = presenting? originFrame : herbView.frame let finalFrame = presenting ? herbView.frame : OriginFrame // Set shrinkage ratio let xScaleFactor = presenting? Initialframe. width/finalframe. width: finalFrame.width / initialFrame.width let yScaleFactor = presenting ? initialFrame.height / finalFrame.height : finalFrame.height / initialFrame.height let scaleTransform = CGAffineTransformMakeScale(xScaleFactor, YScaleFactor) // Set the original position of herbView when presenting {herbview. transform = scaleTransform HerbView. center = CGPoint(x: CGRectGetMidX(initialFrame), y: CGRectGetMidY(initialFrame)) herbView.clipstobounds = true} ContainerView.addSubView (toView ContainerView. BringSubviewToFront (herbView) / / add a elastic effect UIView. AnimateWithDuration (duration, delay: 0.0, usingSpringWithDamping: 0.4, initialSpringVelocity: 0.0, options: [], animations: { () -> Void in herbView.transform = self.presenting ? CGAffineTransformIdentity : scaleTransform herbView.center = CGPoint(x: CGRectGetMidX(finalFrame), y: CGRectGetMidY(finalFrame)) }) { (_) -> Void in transitionContext.completeTransition(true) } }Copy the code

The effect is as follows:




A closer look reveals that both the presentViewController and the dismissViewController view pop up from the top left corner. Because we set originFrame to cgRect.zero.







5. Realization of the final effect.

We just set the originFrame to cgRect. zero, but we can get the original size of the target controller so that it doesn’t pop up in the upper left corner.

1. Add the following code to ViewController’s Extension
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? { transition.originFrame = selectedImage! .superview! .convertRect(selectedImage! .frame, toView: nil) transition.presenting = true selectedImage! .hidden = true return transition } func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { transition.presenting = false return transition }Copy the code
2. When dismiss, the picture just clicked will disappear, because we set it to hide in modal, so the selectedImage should be displayed after the completion of dismiss. You can do this by adding a closure to the PopAnimator
    var dismissCompletion: (()->())?Copy the code

Then in animateTransition UIView. AnimateWithDuration complete closure call

UIView. AnimateWithDuration (duration, delay: 0.0, usingSpringWithDamping: 1, initialSpringVelocity: 0.0, the options: [], animations: { () -> Void in herbView.transform = self.presenting ? CGAffineTransformIdentity : scaleTransform herbView.center = CGPoint(x: CGRectGetMidX(finalFrame), y: CGRectGetMidY(finalFrame)) }) { (_) -> Void in if ! self.presenting { self.dismissCompletion? () } transitionContext.completeTransition(true) }Copy the code

And then finally in viewDidLoad in ViewController

transition.dismissCompletion = { self.selectedImage! .hidden = false }Copy the code

3. Since the picture on ScrollView has rounded corners, we should also switch the picture rounded corners and the controller’s right Angle in the transition animation. Add the following code to the animateTransition method in PopAnimator:

KeyPath: "cornerRadius") round-fromvalue =! presenting ? 0.0:20.0 /xScaleFactor round. ToValue = presenting? 0.0:20.0 / xScaleFactor round. Duration = duration / 2 herbView. Layer. AddAnimation (round, forKey: nil) herbView.layer.cornerRadius = presenting ? 0.0:20.0 / xScaleFactorCopy the code

The final custom modal transition effect is complete!




Custom modal transition animation

2. Customize push transition animation

Compared with custom modal transfer animation, there are not many scenes of custom modal transfer animation, and the principle is actually similar.

1. Simple push effect.

Push requires a navigation controller, so when you load the controller in the AppDelegate, give it a navigation controller. In the code,ViewController and DetailViewController are the two controllers to switch. Add a gesture listener to the ViewController to jump to.




2. As usual, set the agent of the transition animation

  1. I’m going to create a new inheritance NSObject, UIViewControllerAnimatedTransitioning RevealAnimator file, used to set the transitions of the animation agent method, in the add UIViewControllerAnimatedTransitioning agreement must be in order to realize the two Proxy methods:

    Let animationDuration = 2.0 UINavigationControllerOperation =. Push / / set the transitions animation duration func transitionDuration (transitionContext: UIViewControllerContextTransitioning?) NSTimeInterval {return 0} func animateTransition(transitionContext: UIViewControllerContextTransitioning) { }Copy the code
  2. Take the navigation controller in the viewDidLoad method in the ViewController file and set it as a proxy

    navigationController? .delegate = selfCopy the code

    Add a transition animation controller container property to the ViewController file

     let transition = RevealAnimator()Copy the code

    Add an extension to the ViewController file to implement the proxy method

    extension ViewController : UINavigationControllerDelegate { /** - parameter navigationController: -parameter operation:.push. Pop-parameter fromVC: original controller -parameter toVC: destination controller - returns: NavigationController (navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { transition.operation = operation return transition } }Copy the code



3. Add transitions

  1. Add a variable to RevealAnimator that holds the transitionContext of the animateTransition method, which will be used later

     weak var storedContext: UIViewControllerContextTransitioning?Copy the code

    Add some initialization code to the animateTransition as usual

    let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! ViewController let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! DetailViewController transitionContext.containerView()? .addSubview(toVC.view)Copy the code
  2. 1. Create an RWLogoLayer file and draw the LOGO using bezier curves

    import UIKit class RWLogoLayer { class func logoLayer() -> CAShapeLayer { let layer = CAShapeLayer() Layer. geometryFlipped = true let bezier = UIBezierPath() bezier.moveTopoint (CGPoint(x: 0.0, y: AddCurveToPoint (CGPoint(x: 0.0, y: 66.97), controlPoint1:CGPoint(x: 0.0, y: 0.0), controlPoint2:CGPoint(x: 0.0, y: 0.0)) 0.0, y: 57.06)) bezier.addCurvetopoint (CGPoint(x: 16.0, y: 39.0), controlPoint1: CGPoint(x: 27.68, y: 27.68) 66.97), controlPoint2:CGPoint(x: 42.35, Y: 52.75)) bezier.addCurvetopoint (CGPoint(x: 26.0, Y: 17.0), controlPoint1: CGPoint(x: 17.35, Y: 35.41), controlPoint2:CGPoint(x: 26, Y: 17)) bezier.addlinetopoint (x: 38.0, y: AddLineToPoint (x: 49.0, y: 17.0)) bezier.addlinetopoint (x: 49.0, y: 17.0) AddLineToPoint (x: 0.0, y: 0.0)) bezier.addlinetopoint (x: 0.0, y: 0.0) Path = bezier.cgPath layer.bounds = CGPathGetBoundingBox(layer.path) return layer}}Copy the code



  3. Set the RWLogoLayer’s position size and add it to fromVC and toVC respectively in ViewController viewDidAppear

         logo.position = CGPoint(x: view.layer.bounds.size.width/2,
             y: view.layer.bounds.size.height/2 + 30)
         logo.fillColor = UIColor.whiteColor().CGColor
         view.layer.addSublayer(logo)Copy the code

    Add in the viewDidLoad of the DetailViewController

    maskLayer.position = CGPoint(x: view.layer.bounds.size.width/2, y: view.layer.bounds.size.height/2)
         view.layer.mask = maskLayerCopy the code

    There’s a little bit of notice here, so make sure you add

    override func viewDidAppear(animated: Bool) {
         super.viewDidAppear(animated)
    
         view.layer.mask = nil
     }Copy the code

    Remove the mask

  4. Set the animation, added in the animateTransition method of RevealAnimator

    let animation = CABasicAnimation(keyPath: "transform") animation.fromValue = NSValue(CATransform3D: CATransform3DIdentity) // Add a shadow effect animation.toValue = NSValue (CATransform3D: CATransform3DConcat (CATransform3DMakeTranslation (0.0, 10.0, 0.0), CATransform3DMakeScale (150.0, 150.0, 1.0)) animation.duration = animationDuration animation.delegate = self animation.fillMode = kcafillmodeforward animation.removedOnCompletion = false animation.timingFunction = CAMediaTimingFunction(name: KCAMediaTimingFunctionEaseIn) / / at the same time added to the two controllers toVC maskLayer. AddAnimation (animation, forKey: AddAnimation (animation, forKey: nil) // Set a gradient effect for the destination controller let fadeIn = CABasicAnimation(keyPath: AnimationDuration = animationDuration; "opacity") fadein.fromValue = 0.0 fadein.tovalue = 1.0 fadein.duration = animationDuration toVC.view.layer.addAnimation(fadeIn, forKey: nil)Copy the code



    Here the transition effect of push is basically completed, but there will be problems found in the custom modal transition animation, when the transition animation is completed Need to set up transitionContext.com pleteTransition (true), and here, too, this is done to before the pop sort of all the clean up. So rewrite animationDidStop

    override func animationDidStop(anim: CAAnimation, finished flag: Bool) { if let context = storedContext { context.completeTransition(! context.transitionWasCancelled()) let fromVc = context.viewControllerForKey(UITransitionContextFromViewControllerKey) as! ViewController fromVc.logo.removeAllAnimations() } storedContext = nil }Copy the code
  5. Push is done! Now when we hit the start button in the upper left corner and return it crashes because we didn’t make a judgment in the animateTransition method of a RevealAnimator about whether it’s a push or a pop. Define a property to determine whether it is pop or push, and place the animateTransition push code in the push statement

     var operation: UINavigationControllerOperation = .PushCopy the code
         if operation == .Push { // push
    
         }else { // pop
    
         }Copy the code
  6. Add a zoom effect to pop

    let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)! let toView = transitionContext.viewForKey(UITransitionContextToViewKey)! transitionContext.containerView()? InsertSubview (toView belowSubview: fromView) UIView. AnimateWithDuration (animationDuration, delay: 0.0, the options: CurveEaseIn, animations: {fromView. Transform = CGAffineTransformMakeScale (0.01, 0.01)}, completion: {_ in transitionContext.completeTransition(! transitionContext.transitionWasCancelled()) })Copy the code



At this point, you’re done! But there is still some distance from the final effect!

4. Switch the controller according to the sliding gesture

  1. I’m going to put a label at the bottom of the ViewController, and slide to unlock, just as a reminder, we’re going to add a swipe gesture to the whole view.
  2. Change the click listen event of the ViewController to the drag event.
     let pan = UIPanGestureRecognizer(target: self, action: Selector("didPan:"))
         view.addGestureRecognizer(pan)Copy the code
    func didPan(recognizer: UIPanGestureRecognizer) {
     }Copy the code
  3. Adjust the progress of the transition animation based on the offset of the slide, that is, if the transition animation can be interactive. All transitions previously started and ended automatically, without any changes in human interaction. So the original method is not enough.

    You need to add it in the ViewController’s Extension
    NavigationController (navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { if ! transition.interactive { return nil } return transition }Copy the code

    Add an attribute in RevealAnimator to set whether or not interactions can be made

    Var interactive = falseCopy the code

    4. Add gesture recognition to the didPan method of the ViewController, which processes part of it and passes the rest to a RevealAnimator

      switch recognizer.state {
         case .Began:
             transition.interactive = true
             navigationController?.pushViewController(DetailViewController(), animated: true)
         default:
             transition.handlePan(recognizer)
         }Copy the code

    5. In the handlePan method of a RevealAnimator, progress is calculated against the offset

    func handlePan(recognizer: UIPanGestureRecognizer) { let translation = recognizer.translationInView(recognizer.view! .superview!) var progress: CGFloat = abs(translation.x / 200.0) progress = min(Max (progress, 0.01), 0.99) switch recognizer.state {case.changed: / / update the current ferry updateInteractiveTransition animation broadcast schedule (progress) case., Cancelled, Ended: if operation == .Push { // push let transitionLayer = storedContext! .containerView()! The layer transitionLayer. BeginTime = CACurrentMediaTime () if the progress < 0.5 {completionSpeed = 1.0 CancelInteractiveTransition () / / stop animated transitions, to return to the from state} else {completionSpeed = 1.0 finishInteractiveTransition () / / Complete the animated transitions, to the to}} else {/ / pop if progress < 0.5 {cancelInteractiveTransition ()} else {finishInteractiveTransition () } // Make the return interactive transition animation nil, reset animation interactive = false default: break}}Copy the code



    Custom push transition animation

Now the custom push transition animation is complete!

Although the transition animation is not difficult, but from beginning to end this set of logic, or a bit cumbersome, some students may see some circles, this is very normal, because knowledge is mesh ah, linear logic expression can not express each connection of mesh knowledge clearly! So, I give the source code below, for those who are interested in studying.

This paper sort the: iOS. Animations. By. Tutorials. V2.0 source: github.com/DarielChen/… If you have any questions, please leave a message at -d