iOS开发-转场动画切换界面(类似系统动画)
在开发中,无论我们使用 push 还是 present 推出新的 viewcontroller 时,系统为了提高用户体验都会为我们默认加上一些过渡动画。但是开发中需要自定义过度动画效果。这里就需要用到了转场动画了。
如图所示


一、转场动画相关协议
自定义转场动画时需要使用的协议:
- UIViewControllerAnimatedTransitioning: 实现此协议的实例控制转场动画效果。
- UIViewControllerInteractiveTransitioning: 实现此协议的实例控制着利用手势过渡时的进度处理。
二、实现转场动画代码
2.1、实现UIViewControllerAnimatedTransitioning协议
实现转场动画时候,实现UIViewControllerAnimatedTransitioning协议
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext;
在区分Push与Pop操作时候,需要用到以下的
UIView *containerView = [transitionContext containerView];
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIView *fromView = fromVC.view;
    
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *toView = toVC.view;
注意:在转场过程中,转场过渡的容器,如果是PUSH时候,A->B是,fromVC是A,toVC是B;如果是POP的时候,fromVC是B,toVC是A。
动画实现
 在转场过程中更改fromView与toView的toFrame,动画结束后调用transitionContext的completeTransition方法。
具体代码如下
INPushPopTranstioning.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "INPercentDrivenInteractiveTransition.h"
@interface INPushPopTranstioning : NSObject<UIViewControllerAnimatedTransitioning, UINavigationControllerDelegate>
// 是否是Push
@property (nonatomic, assign) NSTimeInterval transitionDuration;
@property (nonatomic, strong) INPercentDrivenInteractiveTransition *interactiveTransition;
+ (instancetype)sharedInstance;
@end
INPushPopTranstioning.m
#import "INPushPopTranstioning.h"
#define kINScreenWidth [[UIScreen mainScreen] bounds].size.width
static INPushPopTranstioning *_sharedInstance = nil;
@interface INPushPopTranstioning ()
// 是否是Push
@property (nonatomic, assign) BOOL isPush;
@end
@implementation INPushPopTranstioning
+ (instancetype)sharedInstance {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedInstance = [[INPushPopTranstioning alloc] init];
        _sharedInstance.transitionDuration = 0.35;
        _sharedInstance.interactiveTransition = [[INPercentDrivenInteractiveTransition alloc] init];
    });
    
    return _sharedInstance;
}
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                                   interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController {
    return self.interactiveTransition.isInteractive?self.interactiveTransition:nil;
}
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
    return self.transitionDuration;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    
    // 转场过渡的容器 如果是PUSH时候,A->B是,fromVC是A,toVC是B;如果是POP的时候,fromVC是B,toVC是A。
    // Transition container
    UIView *containerView = [transitionContext containerView];
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIView *fromView = fromVC.view;
    
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *toView = toVC.view;
    UIImage *fromImage = [self screenShotImage:fromView];
    
    UIImageView *backView = [[UIImageView alloc] initWithFrame:CGRectZero];
    //backView.image = fromImage;
    backView.backgroundColor = [UIColor blackColor];
    backView.frame = containerView.bounds;
    
    UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
    UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
    effectView.frame = backView.bounds;
    //[backView addSubview:effectView];
    UIImageView *viewMaskView = [[UIImageView alloc] initWithFrame:CGRectZero];
    viewMaskView.backgroundColor = [UIColor blackColor];
    viewMaskView.frame = containerView.bounds;
    
    // 判断是 push 还是 pop 操作
    // Determine if it is a push or pop operation
    if (self.isPush) {
        //[containerView addSubview:backView];
        [containerView addSubview:fromView];
        [containerView addSubview:viewMaskView];
        [containerView addSubview:toView];
    } else {
        //[containerView addSubview:backView];
        [containerView addSubview:toView];
        [containerView addSubview:viewMaskView];
        [containerView addSubview:fromView];
    }
    
    
    CGRect fromFrame;
    CGRect toFrame;
    CGFloat beginAlpha = 0.0;
    if (self.isPush) {
        fromFrame = containerView.bounds;
        fromView.frame = fromFrame;
        
        toFrame = CGRectMake(containerView.bounds.size.width, 0, containerView.bounds.size.width, containerView.bounds.size.height);
        toView.frame = toFrame;        
        beginAlpha = 0.0;
    } else {
        fromFrame = containerView.bounds;
        fromView.frame = fromFrame;
        
        toFrame = CGRectMake(containerView.bounds.size.width, 0, containerView.bounds.size.width, containerView.bounds.size.height);
        
        toView.frame = fromFrame;
        
        beginAlpha = 0.3;
    }
    viewMaskView.alpha = beginAlpha;
    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
        if (self.isPush) {
            //fromView.transform = CGAffineTransformMakeScale(0.9, 0.9);
            toView.frame = fromFrame;
            viewMaskView.alpha = 0.3;
        } else {
            fromView.frame = toFrame;
            //toView.transform = CGAffineTransformMakeScale(1.0, 1.0);
            viewMaskView.alpha = 0.0;
        }
    } completion:^(BOOL finished) {
        BOOL cancelled = [transitionContext transitionWasCancelled];
        
        fromView.transform = CGAffineTransformIdentity;
        toView.transform = CGAffineTransformIdentity;
        // 删除遮罩
        [backView removeFromSuperview];
        [viewMaskView removeFromSuperview];
        
        // 设置 transitionContext 通知系统动画执行完毕
        [transitionContext completeTransition:!cancelled];
    }];
}
- (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                   animationControllerForOperation:(UINavigationControllerOperation)operation
                                                fromViewController:(UIViewController *)fromVC
                                                  toViewController:(UIViewController *)toVC {
    if (operation == UINavigationControllerOperationPush) {
        self.isPush = YES;
    } else {
        self.isPush = NO;
    }
    return self;
}
- (UIImage *)screenShotImage:(UIView *)view {
    // 第一个参数表示区域大小。第二个参数表示是否是非透明的。如果需要显示半透明效果,需要传NO,否则传YES。第三个参数就是屏幕密度了,设置为[UIScreen mainScreen].scale可以保证转成的图片不失真。
    UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO,[UIScreen mainScreen].scale);
    if ([view respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) {
        [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES];
    } else {
        [view.layer renderInContext:UIGraphicsGetCurrentContext()];
    }
    UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
    
    UIGraphicsEndImageContext();
    
    return viewImage;
}
// Called when the navigation controller shows a new top view controller via a push, pop or setting of the view controller stack.
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    NSLog(@"willShowViewController:%@",viewController);
}
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    NSLog(@"didShowViewController:%@",viewController);
}
- (CATransform3D)transformRotation:(CGFloat)angle {
    CATransform3D rotation3DIdentity = CATransform3DIdentity;
    rotation3DIdentity.m34 = 0.3/500.0;
    CATransform3D rotateTransform = CATransform3DRotate(rotation3DIdentity, angle, 0, 1, 0);
    CATransform3D transform = CATransform3DMakeTranslation(0, 0, kINScreenWidth/4.0);
    return CATransform3DConcat(rotateTransform, transform);
}
- (void)resetAnChorPoint:(UIView *)view anchorPoint:(CGPoint)anchorPoint {
    CGPoint oldAnchorPoint = view.layer.anchorPoint;
    view.layer.anchorPoint = anchorPoint;
    [view.layer setPosition:CGPointMake(view.layer.position.x + view.layer.bounds.size.width * (view.layer.anchorPoint.x - oldAnchorPoint.x), view.layer.position.y + view.layer.bounds.size.height * (view.layer.anchorPoint.y - oldAnchorPoint.y))];
}
@end
2.2、处理手势过渡
在Pop可能需要手势滑动返回,我这里使用继承UIPercentDrivenInteractiveTransition的子类INPercentDrivenInteractiveTransition来处理
实现拖拽手势UIPanGestureRecognizer
代码如下
INPercentDrivenInteractiveTransition.h
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface INPercentDrivenInteractiveTransition : UIPercentDrivenInteractiveTransition
@property (nonatomic, assign, readonly) BOOL isInteractive;
@end
NS_ASSUME_NONNULL_END
INPercentDrivenInteractiveTransition.m
#import "INPercentDrivenInteractiveTransition.h"
// 屏幕宽与高
#define kScreenWidth  ([UIScreen mainScreen].bounds.size.width)
#define kScreenHeight ([UIScreen mainScreen].bounds.size.height)
@interface INPercentDrivenInteractiveTransition()
@property (nonatomic, assign, readwrite) BOOL isInteractive;
@end
@implementation INPercentDrivenInteractiveTransition
- (instancetype)init
{
    self = [super init];
    if (self) {
        self.isInteractive = NO;
    }
    return self;
}
- (void)handleGesture:(UIPanGestureRecognizer *)panGesture {
    CGFloat transitionX = [panGesture translationInView:panGesture.view].x;
    CGFloat persent = transitionX / panGesture.view.frame.size.width;
    
    switch (panGesture.state) {
        case UIGestureRecognizerStateBegan:
            self.isInteractive = YES;
            [self start];
            break;
        case UIGestureRecognizerStateChanged:
            [self updateInteractiveTransition:persent];
            break;
        case UIGestureRecognizerStateEnded:
            self.isInteractive = NO;
            if (persent > 0.5) {
                [self finishInteractiveTransition];
            }else{
                [self cancelInteractiveTransition];
            }
            break;
        default:
            break;
    }
}
- (void)start {
    UIViewController *controller = [self getCurrentWindowController];
    [controller.navigationController popViewControllerAnimated:YES];
}
/**
 获取当前屏幕显示的controller
 @return controller
 */
- (UIViewController *)getCurrentWindowController {
    UIViewController *result = nil;
    UIWindow * window = [[UIApplication sharedApplication] keyWindow];
    if (window.windowLevel != UIWindowLevelNormal) {
        NSArray *windows = [[UIApplication sharedApplication] windows];
        for(UIWindow * tmpWin in windows) {
            if (tmpWin.windowLevel == UIWindowLevelNormal) {
                window = tmpWin;
                break;
            }
        }
    }
    
    result = window.rootViewController;
    
    while (result.presentingViewController) {
        result = result.presentingViewController;
    }
    
    if ([result isKindOfClass:[UITabBarController class]]) {
        result = [(UITabBarController *)result selectedViewController];
    }
    
    if ([result isKindOfClass:[UINavigationController class]]) {
        result = [(UINavigationController *)result visibleViewController];
    }
    return result;
}
@end
2.3、UINavigationController扩展Category
在Push与Pop时候需要转场动画,那需要替换UINavigationController中的方法
需要使用method_exchangeImplementations
这里暂不详细说明方法替换的逻辑了,详细参考
https://blog.csdn.net/gloryFlow/article/details/131677505
具体代码如下
UINavigationController+Transition.h
#import <UIKit/UIKit.h>
#import "UIViewController+Transition.h"
/**
 处理转场动画
 */
@interface UINavigationController (Transition)
@end
UINavigationController+Transition.m
#import "UINavigationController+Transition.h"
#import <objc/runtime.h>
#import "INPushPopTranstioning.h"
@implementation UINavigationController (Transition)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        method_exchangeImplementations(class_getInstanceMethod(class, @selector(popViewControllerAnimated:)), class_getInstanceMethod(class, @selector(in_popViewControllerAnimated:)));
        method_exchangeImplementations(class_getInstanceMethod(class, @selector(pushViewController:animated:)), class_getInstanceMethod(class, @selector(in_pushViewController:animated:)));
    });
}
- (void)in_pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
    if (animated && [INPushPopTranstioning sharedInstance].transitionDuration > 0.0) {
        // 跳转到某些ViewController,需要特定的转场动画,需要设置showNavTransitionDelegate。
        if (viewController.showNavTransitionDelegate) {
            self.delegate = viewController.showNavTransitionDelegate;
        } else {
            self.delegate = [INPushPopTranstioning sharedInstance];
        }
    }
    
    [self in_pushViewController:viewController animated:animated];
}
- (nullable UIViewController *)in_popViewControllerAnimated:(BOOL)animated {
    if (animated && [INPushPopTranstioning sharedInstance].transitionDuration > 0.0) {
        // 回到到某些ViewController,需要特定的转场动画,需要设置showNavTransitionDelegate。
        if (self.visibleViewController.showNavTransitionDelegate) {
            self.delegate = self.visibleViewController.showNavTransitionDelegate;
        } else {
            self.delegate = [INPushPopTranstioning sharedInstance];
        }
    }
    return [self in_popViewControllerAnimated:animated];
}
@end
2.4、UIViewController扩展Category
我这里扩展UIViewController添加运行时属性,代码如下
UIViewController+Transition.h
#import <UIKit/UIKit.h>
@interface UIViewController (Transition)
@property (nonatomic, weak) id <UINavigationControllerDelegate> showNavTransitionDelegate;
@end
UIViewController+Transition.m
#import "UIViewController+Transition.h"
#import <objc/runtime.h>
static const void *showTransitionKey = &showTransitionKey;
@implementation UIViewController (Transition)
- (id<UINavigationControllerDelegate>)showNavTransitionDelegate {
    return objc_getAssociatedObject(self, showTransitionKey);
}
- (void)setShowNavTransitionDelegate:(id<UINavigationControllerDelegate>)showNavTransitionDelegate {
    objc_setAssociatedObject(self, showTransitionKey, showNavTransitionDelegate, OBJC_ASSOCIATION_ASSIGN);
}
@end
2.4、UIViewController扩展Category Navigation
UIViewController+Navigation.h
#import <UIKit/UIKit.h>
@interface UIViewController (Navigation)
@property (nonatomic, weak) UINavigationController *in_navigationController;
@end
UIViewController+Navigation.m
#import "UIViewController+Navigation.h"
#import <objc/runtime.h>
static const void *showNavTransitionKey = &showNavTransitionKey;
@implementation UIViewController (Navigation)
- (UINavigationController *)in_navigationController {
    return objc_getAssociatedObject(self, showNavTransitionKey);
}
- (void)setIn_navigationController:(UINavigationController *)in_navigationController {
    objc_setAssociatedObject(self, showNavTransitionKey, in_navigationController, OBJC_ASSOCIATION_ASSIGN);
}
@end
三、使用自定义转场动画
在BaseViewController中添加拖拽手势,用于处理手势滑动返回。
UIScreenEdgePanGestureRecognizer *pan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:[INPushPopTranstioning sharedInstance].interactiveTransition action:@selector(handleGesture:)];
    pan.edges = UIRectEdgeLeft;
    [self.view addGestureRecognizer:pan];
BaseViewController完整代码如下
INBaseViewController.h
#import <UIKit/UIKit.h>
#import "UIViewController+Navigation.h"
NS_ASSUME_NONNULL_BEGIN
@interface INBaseViewController : UIViewController
@property (nonatomic, assign) BOOL isViewDidAppear;
@end
NS_ASSUME_NONNULL_END
INBaseViewController.m
#import "INBaseViewController.h"
#import "INPushPopTranstioning.h"
@interface INBaseViewController ()
@end
@implementation INBaseViewController
- (id)init {
    self = [super init];
    if (self) {
        self.hidesBottomBarWhenPushed = YES;
        [self setNeedsStatusBarAppearanceUpdate];
    }
    return self;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.exclusiveTouch = YES;
    self.view.clipsToBounds = YES;
    
    self.automaticallyAdjustsScrollViewInsets = NO;
    self.extendedLayoutIncludesOpaqueBars = YES;
    
    UIScreenEdgePanGestureRecognizer *pan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:[INPushPopTranstioning sharedInstance].interactiveTransition action:@selector(handleGesture:)];
    pan.edges = UIRectEdgeLeft;
    [self.view addGestureRecognizer:pan];
}
- (void)loadView {
    [super loadView];
}
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    self.isViewDidAppear = NO;
}
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    self.isViewDidAppear = NO;
}
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    self.isViewDidAppear = YES;
    self.navigationController.interactivePopGestureRecognizer.enabled = YES;
}
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    self.isViewDidAppear = NO;
}
- (void)handleGesture:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer {
    // 手势
}
#pragma mark - StatusBar style
- (UIStatusBarStyle)preferredStatusBarStyle {
    return UIStatusBarStyleDefault;
}
- (BOOL)prefersStatusBarHidden {
    return NO;
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
    return (toInterfaceOrientation == UIInterfaceOrientationPortrait);
}
- (BOOL)shouldAutorotate {
    return NO;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
    return UIInterfaceOrientationPortrait;
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
- (void)dealloc {
    NSLog(@"DEALLOCBaseViewController");
}
@end
我这里使用的是NavigationController嵌套自定义的TabbarController.
在AppDelegate中didFinishLaunchingWithOptions进行设置
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    [self.window makeKeyAndVisible];
    
    
    INMainTabBarController *tabbar = [[INMainTabBarController alloc] init];
    UINavigationController *mainNav = [[UINavigationController alloc] initWithRootViewController:tabbar];
    mainNav.delegate = [INPushPopTranstioning sharedInstance];
    
    self.window.backgroundColor = [UIColor whiteColor];
    self.window.rootViewController = mainNav;
    
    return YES;
}
三、小结
iOS开发-转场动画切换界面(类似系统动画)。UIViewControllerAnimatedTransitioning与UIViewControllerInteractiveTransitioning。
学习记录,每天不停进步。









![心法利器[92] | 谈校招:刷题和笔试准备](https://img-blog.csdnimg.cn/img_convert/5632a04c9c3e186ffc69a355b11ad811.png)








