Project overview
- The most common animation in iOS is undoubtedly the transition animation of Push and Pop, followed by the transition animation of Present and disass.
If we want to customize these transition animations, apple actually provides relevant API s. Before customizing the transition, we need to understand the transition principle and processing logic. The following is the effect of custom transition:

- Project address: CustomPushAndPresent
If articles and projects are helpful to you, please give a Star ⭐ Hello, your Star ⭐ It is the driving force that I continue to output, thank you 😘
Push/Pop transition
Push/Pop transition principle
- Before calling pushViewController:animated: of the navigation controller, if the delegate object of the navigation controller is set, the callback method of the delegate object will be called navigationcontroller: animationcontrolerforoperation: fromviewcontroller: toviewcontroller:, and the transition can be customized in this callback method, The callback method needs to return an object that complies with uiviewcontrolleranimatedtransition protocol. Define a class to implement two methods of uiviewcontrolleranimatedtransition protocol in order to customize Push/Pop transition. The two methods that must be implemented are as follows:
@interface HRPushAnimatedTransitioning : NSObject <UIViewControllerAnimatedTransitioning,CAAnimationDelegate> @property(nonatomic, assign)UINavigationControllerOperation operation; @end - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext; - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
- Provide a property HR to UIViewController with runtime_ Addtransitionflag, used to mark whether to add a custom transition. The code is as follows:
@interface UIViewController (TransitionProperty) @property (nonatomic, assign) BOOL hr_addTransitionFlag;//Add custom transition @end #import "UIViewController+TransitionProperty.h" #import <objc/runtime.h> static NSString *hr_addTransitionFlagKey = @"hr_addTransitionFlagKey"; @implementation UIViewController (TransitionProperty) - (void)setHr_addTransitionFlag:(BOOL)hr_addTransitionFlag { objc_setAssociatedObject(self, &hr_addTransitionFlagKey, @(hr_addTransitionFlag), OBJC_ASSOCIATION_ASSIGN); } - (BOOL)hr_addTransitionFlag { return [objc_getAssociatedObject(self, &hr_addTransitionFlagKey) integerValue] == 0 ? NO : YES; } @end
As mentioned above, as long as you set delegate for the navigation controller, after calling pushViewController:animated:, the navigationController:animationControllerForOperation:fromViewController:toViewController: method will be executed to display the custom Push/Pop transition. The same is true after calling popViewControllerAnimated:. The code of the navigation controller is as follows:
-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated{ /*Set delegate to the navigation controller, and call pushViewController:animated:, Will execute navigationController:animationControllerForOperation:fromViewController:toViewController: */ self.delegate = (id)viewController; [super pushViewController:viewController animated:animated]; } -(UIViewController *)popViewControllerAnimated:(BOOL)animated{ /*After setting delegate for the navigation controller and calling popViewControllerAnimated:, Will execute navigationController:animationControllerForOperation:fromViewController:toViewController: */ self.delegate = self.viewControllers.lastObject; return [super popViewControllerAnimated:animated]; }
Custom transition
- Here, you can customize the transition of toView from the top of the screen to the center of the screen during Push, and the transition of toView from the center of the screen to the center of the screen during Pop. The implementation code is as follows:
#import <UIKit/UIKit.h> @interface HRPushAnimatedTransitioning : NSObject <UIViewControllerAnimatedTransitioning,CAAnimationDelegate> @property(nonatomic, assign)UINavigationControllerOperation operation; @end @implementation HRPushAnimatedTransitioning -(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{ return 0.4; } -(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{ UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey]; UIView *containerView = transitionContext.containerView; //containerView originally has fromView, just add toView [containerView addSubview:toView]; CGRect fromViewStartFrame = [transitionContext initialFrameForViewController:fromVC]; CGRect toViewStartFrame = [transitionContext finalFrameForViewController:toVC]; CGRect fromViewEndFrame = fromViewStartFrame; CGRect toViewEndFrame = toViewStartFrame; if (_operation == UINavigationControllerOperationPush) { toViewStartFrame.origin.y -= toViewEndFrame.size.height; }else if (_operation == UINavigationControllerOperationPop) { fromViewEndFrame.origin.y += fromViewStartFrame.size.height; [containerView sendSubviewToBack:toView]; } fromView.frame = fromViewStartFrame; toView.frame = toViewStartFrame; [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ fromView.frame = fromViewEndFrame; toView.frame = toViewEndFrame; } completion:^(BOOL finished) { [transitionContext completeTransition:!transitionContext.transitionWasCancelled]; }]; }
Right slide return gesture of processing system
- At the beginning of iOS7, apple provided a gesture of sliding back to the previous interface. Because I set the delegate of the navigation controller in the pushViewController:animated: method, the right sliding back gesture is invalid. The solution is to reset the delegate object of the right sliding back gesture:
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. __weak typeof(self) weakself = self; if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)]) { /*As long as the leftBarButtonItem or navigationController of the navigationItem is customized, the sliding gesture will be invalid. Therefore, reset the proxy of the system's own right slide return gesture to self */ self.interactivePopGestureRecognizer.delegate = weakself; } }
After the above settings, the rootViewController will also respond to the right slide back, which may cause some problems. Therefore, it is necessary to disable the right slide back function of the rootViewController. That is, the codes in the navigation controller are as follows:
#pragma mark - UIGestureRecognizerDelegate -(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{ if (gestureRecognizer == self.interactivePopGestureRecognizer) { //Shield the sliding return gesture of the rootViewController to avoid the crash caused by the right sliding return gesture if (self.viewControllers.count <= 1 || self.visibleViewController == [self.viewControllers objectAtIndex:0]) { return NO; } } return YES; }
Note that the right slide back gesture is enabled by default, that is, the enable of self.interactivePopGestureRecognizer is YES by default
Handle the transition of the right slide return gesture
- Although the custom Push/Pop transition is implemented above, the system's own sliding gesture pop does not display our custom Push/Pop transition effect, but the system's default transition effect is still displayed.
The reason is that when the transition of Push or Pop is customized, the system calls the navigationController:animationControllerForOperation:fromViewController:toViewController: method. If the method returns a non nil object, it will execute the following proxy methods:
-(id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController
This is a proxy method provided by apple for developers to customize the interactive transition of sliding gestures. It returns an object that complies with the uiviewcontrollerinteractivetransition protocol. The object needs to implement the startinteractive transition: method. Therefore, apple provides a UIPercentDrivenInteractiveTransition class that implements the protocol, We only need to define a class that inherits the UIPercentDrivenInteractiveTransition class to meet the conditions for returning objects, instead of implementing the startInteractiveTransition: method.
When the object returned by the navigationController:animationControllerForOperation:fromViewController:toViewController is not nil, push and pop will call back the navigationController:interactionControllerForAnimationController: proxy method, and we rewrite the proxy method only for the transition of the right slide return gesture, and nil will be returned in other cases, Therefore, it is necessary to distinguish between push and pop. The solution is to save whether to push or pop in navigationController:animationControllerForOperation:fromViewController:toViewController. The code is as follows:
//Used to customize the transition of Push or Pop //The return value of non nil indicates that the custom Push or Pop transition is used. Nil indicates that the system default transition is used -(id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC{ if (!self.hr_addTransitionFlag) { return nil; } HRPushAnimatedTransitioning *obj = [[HRPushAnimatedTransitioning alloc] init]; obj.operation = operation; _operation = operation; if (operation == UINavigationControllerOperationPush) { // NSLog(@"_interactive:%@--%@", _interactive, self); if (_interactive == nil) { _interactive = [[HRPercentDrivenInteractiveTransition alloc] init]; } [_interactive addGestureToViewController:self]; } return obj; } //This method will be called back only after using the custom Push or Pop transition, which is used to customize the transition interaction mode of sliding gesture //The return value non nil indicates that the transition progress can be processed interactively. Nil indicates that the transition progress cannot be processed interactively, and the transition can be completed directly -(id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController{ if (_operation == UINavigationControllerOperationPush) { return nil; }else{ if (_interactive.canInteractive) { return _interactive; }else{ return nil; } } }
Realize the transition of custom right sliding return gesture
- The logic of HRPercentDrivenInteractiveTransition class is: add a Pan gesture to the controller view. When sliding right, calculate the percentage of the right sliding in the screen width (which can be considered as transition progress parameter), and then call popViewControllerAnimated: of the navigation controller when sliding right. updateInteractiveTransition: is invoked during sliding, and the progress parameter percent is passed in. At the end of transition, judge whether to call finishInteractiveTransition (transition is completed, i.e. successfully pop to the previous interface) or cancel interactive transition (transition is restored to the starting point) according to the transition progress. The final code is as follows:
#import <UIKit/UIKit.h> //UIPercentDrivenInteractiveTransition implements uiviewcontrollerinteractivetransition protocol @interface HRPercentDrivenInteractiveTransition : UIPercentDrivenInteractiveTransition @property (readonly, assign, nonatomic) BOOL canInteractive; -(void)addGestureToViewController:(UIViewController *)vc; @end @interface HRPercentDrivenInteractiveTransition () @property (nonatomic, weak) UINavigationController *nav; @property (nonatomic, strong) CADisplayLink *displayLink; @property (nonatomic, assign) CGFloat percent; @end @implementation HRPercentDrivenInteractiveTransition -(void)addGestureToViewController:(UIViewController *)vc{ UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)]; [vc.view addGestureRecognizer:pan]; self.nav = vc.navigationController; } -(void)panAction:(UIPanGestureRecognizer *)pan{ _percent = 0.0; CGFloat totalWidth = pan.view.bounds.size.width; CGFloat x = [pan translationInView:pan.view].x; _percent = x/totalWidth; switch (pan.state) { case UIGestureRecognizerStateBegan:{ _canInteractive = YES; [_nav popViewControllerAnimated:YES]; } break; case UIGestureRecognizerStateChanged:{ [self updateInteractiveTransition:_percent]; } break; case UIGestureRecognizerStateEnded:{ _canInteractive = NO; [self continueAction]; } break; default: break; } } -(BOOL)isCanInteractive{ return _canInteractive; } - (void)continueAction{ if (_displayLink) { return; } _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(UIChange)]; [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; } - (void)UIChange { CGFloat timeDistance = 1.5/60; if (_percent > 0.4) { _percent += timeDistance; }else { _percent -= timeDistance; } [self updateInteractiveTransition:_percent]; if (_percent >= 1.0) { //Transition completed [self finishInteractiveTransition]; [_displayLink invalidate]; _displayLink = nil; } if (_percent <= 0.0) { //Transfer cancellation [self cancelInteractiveTransition]; [_displayLink invalidate]; _displayLink = nil; } }
Present/Dimiss transition
Present/Dimiss transition principle
- The controller sets transitioningDelegate as itself, abides by the uiviewcontrollertransitingdelegate protocol, and implements the present animation method and disass animation method of the protocol, namely the following two methods:
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source; -(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
These two methods need to return an object that complies with the uiviewcontrolleranimatedtransition protocol, and define a class to implement the two methods of the uiviewcontrolleranimatedtransition protocol in order to customize the Present/Dimiss transition.
The key codes of the controller are as follows:
- (instancetype)init { self = [super init]; if (self) { self.transitioningDelegate = self; } return self; } //present transition animation (non interactive) -(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{ HRPresentAnimatedTransitioning *obj = [[HRPresentAnimatedTransitioning alloc] initType:PictureTransitionPresent]; return obj; } //Disass transition animation (non interactive) -(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{ HRPresentAnimatedTransitioning *obj = [[HRPresentAnimatedTransitioning alloc] initType:PictureTransitionDismiss]; return obj; }
Custom transition
- Here, you can customize the transition of toView from the left to the right of the screen to the center of the screen when Present, and the transition of toView from the center of the screen to the right when disass. The implementation code is as follows:
typedef NS_ENUM(NSInteger,PictureTransitionType) { PictureTransitionPresent = 0,//display PictureTransitionDismiss //disappear }; @interface HRPresentAnimatedTransitioning : NSObject <UIViewControllerAnimatedTransitioning> - (instancetype)initType:(PictureTransitionType)type; @end #import "HRPresentAnimatedTransitioning.h" @interface HRPresentAnimatedTransitioning () @property(nonatomic, assign)PictureTransitionType type; @end @implementation HRPresentAnimatedTransitioning - (instancetype)initType:(PictureTransitionType)type{ self = [super init]; if (self) { _type = type; } return self; } -(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{ return 0.4; } -(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{ //When present, fromVC is the navigation controller and toVC is the HRDetailViewController. When disassiss, fromVC is the HRDetailViewController and toVC is the navigation controller UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey]; UIView *containerView = transitionContext.containerView; //containerView originally has fromView, just add toView [containerView addSubview:toView]; CGRect fromViewStartFrame = [transitionContext initialFrameForViewController:fromVC]; CGRect toViewStartFrame = [transitionContext finalFrameForViewController:toVC]; CGRect fromViewEndFrame = fromViewStartFrame; CGRect toViewEndFrame = toViewStartFrame; if (_type == PictureTransitionPresent) { toViewStartFrame.origin.x -= toViewEndFrame.size.width; }else if (_type == PictureTransitionDismiss) { fromViewEndFrame.origin.x += fromViewStartFrame.size.width; [containerView sendSubviewToBack:toView]; } fromView.frame = fromViewStartFrame; toView.frame = toViewStartFrame; [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ fromView.frame = fromViewEndFrame; toView.frame = toViewEndFrame; } completion:^(BOOL finished) { [transitionContext completeTransition:!transitionContext.transitionWasCancelled]; }]; } @end