美文网首页
自定义VCStack

自定义VCStack

作者: chieryw | 来源:发表于2018-12-27 15:05 被阅读1次

    背景介绍

    最近开发的几个工程使用的都是系统的VCStack,即UITabbarController + UINavigationController的方式。这是一个经典的组合,在现实的开发场景中基本已经能够满足需求。但是,最近几期UI稿和UE稿的设计规则,有点超出了这个既有框架的能力

    • 遮罩全屏的半浮层
    • 出栈入栈复杂的动画
    • 跨VC堆栈的poppush操作

    在现有的UITabbarController + UINavigationController结构下,这些功能已经被实现,但是过程较为复杂,不少逻辑现在看来任有优化的空间,基于这个背景,打算写一个自定义的VCStack,解决系统空间的局限性

    系统VCStack存在的困境

    在构思自定义VCStack之前,回顾了一下系统控件在日常开发中存在的瓶颈,这些瓶颈在日查那个的业务开发中经常困扰着我们,拖累开发人员的效率。总结了一下,有以下几点:

    • UI***Bar层级过高导致的页面遮挡问题
    • 出/入栈动画支持不够友好的问题
      是的,我们可以通过NavigationControllerDelegate的方式,在代理中完成自定义动画的实现。但是这个代理的接入往往强依赖在某一个页面,抽象的层次不够,复用性也不高。不符合要求
    • 任意时间点getTopVC带来的问题
      堆栈的操作往往伴随着动画,动画中包含时间,如果我们在不合适的时间节点getTopVC可能导致之后的UI操作完全失效。比如,view正在消失的时候获取topVC并在vc.view中增加UI的处理
    • 布局标准的问题。
      由于TopLayout和BottomLayout的存在,导致我们的布局原点在一些操作中可能发生改变,这样的情况需要一定的开发经验才能捕捉到。一旦人为遗漏就可能造成布局上的错误
    • 交叉影响。
      这里举一个例子:修改Navigation的backItem会导致系统默认的优化手势失效,需要复写此功能才能生效
    • 指定堆栈的跳转。
      系统当前没有提供一个统一的调度入口来解决跨VC的跳转的问题,当前的实现还是基于遍历来找到VC实现跳转
    • 模态视图继续跳转的问题
      这是一种经常出现的场景,模态一个视图,在这个模态视图的基础上还存在堆栈的操作。当前的实现大多是在模态的基础上再包一层NavigationController,让其具备堆栈操作的能力

    上面几个case使我们自定义VCStack解决的核心问题,本文也会按照这几个痛点展开讲解是如何一一解决这些问题的

    自定义VCStack是什么

    先交代一下这个VCStack到底是什么,系统NavigationController的效果我们都不陌生,如何在不继承系统NavigationController的基础上实现一套自己的VCStack管理机制呢(保持效果一致的原则)?从日常的使用中,我们了解到系统的NavigationController其实一个堆栈管理器,之中最重要的是VC的管理,可能是顶层封装的原因使得我们对整个管理体系了解不多。但是有几点是可以猜测到的

    1、所有的VC都拥有自己的View
    2、所有的View都是在根Window上展示的
    3、你看到的动画只是管理器让交互不再生硬做出的表象

    意识到这三点,接下来就好办了,VC是独立的,可以在任意节点创建和销毁,我们的VCStack只需要管理他们的显示逻辑和已有的生命周期。所以VCStack只要找到切合的时间点叠加和管理这些VC即可。首先有个统一的入口

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    

    这个节点中window需要一个rootViewController,这是VCStack接入的切口,一个VC创建并作为RootViewController被VCStack持有,VCStackInstance.rootViewController作为参数给到Window。这一步操作已经为VCStack打下了基石,因为之后所有VC.view的叠加都有了rootView.接下来的事情就变的简单了

    1、push操作将vc.view叠加到currentVC
    2、pop操作将vc.view从上一个vc.view移除

    这期间需要兼顾的东西还有很多,比如

    1、vc生命周期的一致
    2、手势操作
    3、动画接入

    对整个想做的事情有了一定的了解了之后,下面是一些实现中的细节

    逐个击破

    视图层级 + 布局原点

    自定义VCStack不会再有TopLayout和BottomLayout这种预置依赖,所有的View的布局都将从window的(0,0)点开始布局。navigationBarTabBar也将会被CustomView代替以此抹平层级间Z轴差距过大导致的遮罩问题
    [图片上传中...(系统navigation层级.png-6d0e8b-1545878789170-0)]

    自定义层级.png

    当Window的整个区域都有权限去管理之后,层级和布局原点的问题就已经不是问题了,但是这样又引入了其他问题:

    1. 自定义navigationBar增加了每个页面开发的成本
    2. 自定义TabBar增加了每个页面开发的成本

    一个好的方法就是创建一个快捷的模板类,将常用的NavigationBar和常用的TabBar封装成模板输出,增加开发效率

    @interface UIViewController (NavigationBar)
    - (HDDefaultNaviBar *)defaultBar;
    @end
    
    - (HDDefaultNaviBar *)defaultBar {
        HDDefaultNaviBar *customerBar = [[HDDefaultNaviBar alloc] initWithFrame:CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.navigationBarHeight + HDScreenInfo.statusBarHeight)];
        customerBar.backgroundColor = [UIColor whiteColor];
        customerBar.title = @"测试title";
        customerBar.backIcon = [UIImage imageNamed:@"NaviBack"];
        customerBar.backAction = ^{
            [self.vcStack popWithAnimation:[HDVCStackAnimation defaultAnimation]];
        };
        return customerBar;
    }
    
    
    动画拓展性

    系统的Navigation堆栈的跳转提供的api并不多

    - (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated; // Uses a horizontal slide transition. Has no effect if the view controller is already in the stack
    - (nullable UIViewController *)popViewControllerAnimated:(BOOL)animated; // Returns the popped controller.
    

    跳转中动画的支持方式为Bool值,这就限定了跳转中的动画拓展性。当然,设计系统的人为了能让跳转中的动画得到更高粒度的支持,实现了NavigationControllerDelegate这套协议,在集成了这套协议的VC中,可以将动画拓展的更好,协议如下:

    - (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                       animationControllerForOperation:(UINavigationControllerOperation)operation
                                                    fromViewController:(UIViewController *)fromVC
                                                      toViewController:(UIViewController *)toVC  NS_AVAILABLE_IOS(7_0);
    

    但是任然有缺陷,细想一下,这样的协议是在哪个层面实现呢?

    1、直接耦合到需要动画支持的VC?
    2、抽象到UIViewController层面的统一代理?

    1的方式在实际的使用中,算是较多的一种,但是存在拓展性和逻辑抽象的问题,相同的问题在另一个场景下,大多的复用方式是:copy + 粘贴。场景少还能理解,一旦这样场景多了,这种方式带来的问题就会凸显出来。渐渐的在使用系统VCStack的基调下,就会有人抽象这个层面的信息,做一个统一的管理,形成了2的这种方式,但是,2这种方式也是存在问题的,先看一下抽象层面的信息:

    • currentVC
    • willShowVC
    • operation

    关键点出在了operation,这是系统的枚举类型,和业务场景中的契合度不是很高,限制了动画的类型。这相当于找到了这个动画支持的痛点,现在讲一下我的思路:

    在自定义的VCStack中将动画完全交出去,以实例的形式交出去,这看起来有点难以理解。如何统一实例的api?这就用到了协议。所有的animation实例是继承AnimationProtocol的,由这个协议来约束api,使得所有实例的调度一致。结构如下:


    Animation设计.png

    下面是实例的生成api,在实际的使用中每个独具特色的动画协议都是这么写的,他们的具体实现放在了集成的协议中

    @interface HDVCStackAnimation : NSObject <HDVCStackAnimationProtocol>
    + (instancetype)defaultAnimation;
    @end
    

    协议本身和堆栈的逻辑保持一致

    @protocol HDVCStackAnimationProtocol <NSObject>
    - (void)pushWithWillShowVC:(UIViewController *)willShowVC
                     currentVC:(UIViewController *)currentVC
                    completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);
    
    - (void)popWithWillShowVC:(UIViewController *)willShowVC
                    currentVC:(UIViewController *)currentVC
                   completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);
    
    @end
    

    协议的实现也是面向切面的,只需要关注当前的参数和逻辑,例如如下是一个模拟系统自带的堆栈动画的协议实现

    @implementation HDVCStackAnimation
    + (instancetype)defaultAnimation {
        return [HDVCStackAnimation new];
    }
    
    - (void)pushWithWillShowVC:(UIViewController *)willShowVC
                     currentVC:(UIViewController *)currentVC
                    completion:(void (^)(BOOL))completion {
        // 动画开始前的UI效果
        willShowVC.view.frame = CGRectMake(HDScreenInfo.width, 0, HDScreenInfo.width, HDScreenInfo.height);
        [UIView animateWithDuration:0.34 animations:^{
            willShowVC.view.frame = CGRectMake(HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
            currentVC.view.frame = CGRectMake(- HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
        } completion:^(BOOL finished) {
            if (finished) {
                /* 将对应View的frame还原
                 保持和无动画的逻辑对应
                 同时保证在UI调试时的正确性
                 */
                willShowVC.view.frame = CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.height);
                currentVC.view.frame = CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.height);
            }
            completion(finished);
        }];
    }
    
    - (void)popWithWillShowVC:(UIViewController *)willShowVC
                    currentVC:(UIViewController *)currentVC
                   completion:(void (^)(BOOL))completion {
        // 动画开始前的UI效果
        willShowVC.view.frame = CGRectMake(- HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
        currentVC.view.frame = CGRectMake(HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
        [UIView animateWithDuration:0.34 animations:^{
            willShowVC.view.frame = CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.height);
            currentVC.view.frame = CGRectMake(HDScreenInfo.width, 0, HDScreenInfo.width, HDScreenInfo.height);
        } completion:^(BOOL finished) {
            completion(finished);
        }];
    }
    @end
    

    调用API的简化:

    [self.vcStack pushto:vc animation:[HDVCStackAnimation defaultAnimation]];
    

    可以看到,优化之后的动画api参数也是三个

    • currentVC
    • willShowVC
    • AnimationInstance

    但是这里的animationInstance实现的空间大大增加,他只要继承自AnimationProtocol,具体的animation如何实现已经完全交给了业务层。如果在业务层的设计上适配几套符合当前场景的animation,这样的抽象也会被简化到为数不多的Animation实例中。满足了我们的要求,拓展性和逻辑抽象

    getTopVC + 交叉影响

    在完全接手了VCStack之后,对于操作的每个细节都在开发者的掌握之中,当任务触达的时候,可以追加AnimationCompletionHandle的处理,来让这个逻辑更加健壮。同样的交叉影响的存在也被开发人员决定,只有设计中存在这种交叉影响,才会在使用中存在这样的逻辑。设计的节点已经被开发人员管控,需不需要这种逻辑交互已经不再是一个黑盒

    getTopVC+交叉影响.png
    指定VC的跳转

    这个功能在实际的业务中会经常遇到,在系统Navigation的基础上的实现如下

    1、遍历navigationController.viewControllers
    2、找到匹配的VC实例
    3、执行popToVC操作

    前面两步基本不可避免,导致在实际的落地式往往一堆一堆代码的存在,对于代码简洁来说不是一个很好的方案。考虑到这样的需求场景,VCStack中集成了一套快捷的跳转API,覆盖了常见的业务场景

    /**
     push 操作,向当前堆栈中r压入一个对象
    
     @param vc 即将被入栈的viewController
     @param animation 入栈动画
     */
    - (void)pushto:(UIViewController *)vc
         animation:(NSObject<HDVCStackAnimationProtocol> *)animation;
    
    /**
     出栈操作
    
     @param animation 出栈动画
     */
    - (void)popWithAnimation:(NSObject<HDVCStackAnimationProtocol> *)animation;
    
    /**
     出栈到根节点操作
    
     @param animation 出栈动画类型
     */
    - (void)popToRootViewControllerWithAnimation:(NSObject<HDVCStackAnimationProtocol> *)animation;
    
    /**
     出栈到指定的vc操作,匹配条件是当前的vc名称
    
     @param vcName 即将要显示的vc名称
     @param popAnimation 出栈动画
     */
    - (void)popToVCWithName:(NSString *)vcName
        animation:(NSObject<HDVCStackAnimationProtocol> *)popAnimation;
    
    /**
     出栈到指定的vc,匹配条件是实例对象的id指针是否相等
    
     @param vc 即将要显示的vc实例
     @param popAnimation 出栈动画
     @param popCompletion 操作完成之后的回调,主要用于pop then push这种操作
     */
    - (void)popTo:(UIViewController *)vc
        animation:(NSObject<HDVCStackAnimationProtocol> *)popAnimation
    popCompleteHandle:(void (^)(BOOL))popCompletion;
    
    /**
     出栈到指定的vc名称,之后再压栈到一个的vc
    
     @param popVCName 即将在栈顶出现的vc名称
     @param popAnimation 出栈动画
     @param pushVC 即将压栈的vc实例
     @param pushAnimation 压栈动画
     */
    - (void)popToVCWithName:(NSString *)popVCName
                  animation:(NSObject<HDVCStackAnimationProtocol> *)popAnimation
                 thenPushTo:(UIViewController *)pushVC
                  animation:(NSObject<HDVCStackAnimationProtocol> *)pushAnimation;
    
    /**
     出栈到指定的vc实例,之后再压栈到一个的vc
    
     @param popVC 即将在栈顶出现的vc名称
     @param popAnimation 出栈动画
     @param pushVC 即将压栈的vc实例
     @param pushAnimation 压栈动画
     */
    - (void)popTo:(UIViewController *)popVC
        animation:(NSObject<HDVCStackAnimationProtocol> *)popAnimation
       thenPushTo:(UIViewController *)pushVC
        animation:(NSObject<HDVCStackAnimationProtocol> *)pushAnimation;
    
    @end
    

    逻辑的处理已经在VCStack内部完成,只需要简单的API调用就可以完成业务需求

    模态视图后续堆栈跳转

    如果在模态视图中还存在堆栈的跳转,系统VCStack基础下的处理基本是在modalVC上包装一层VCStack,使其具备这样的能力,但是这里会存在问题,两个navigationStack的间接断开,如果这里执行popToVC会带了大量的逻辑判断。使用了自定义VCStack可以将modal视图的出现规划到push操作中,只是这里的动画实例发生了改变

    @implementation HDModelAnimation
    + (instancetype)defaultAnimation {
        return [HDModelAnimation new];
    }
    
    - (void)pushWithWillShowVC:(UIViewController *)willShowVC
                     currentVC:(UIViewController *)currentVC
                    completion:(void (^)(BOOL))completion {
        // 动画开始前的UI效果
        willShowVC.view.frame = CGRectMake(0, HDScreenInfo.height, HDScreenInfo.width, HDScreenInfo.height);
        [UIView animateWithDuration:0.34 animations:^{
            willShowVC.view.frame = CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.height);
        } completion:^(BOOL finished) {
            completion(finished);
        }];
    }
    
    - (void)popWithWillShowVC:(UIViewController *)willShowVC
                    currentVC:(UIViewController *)currentVC
                   completion:(void (^)(BOOL))completion {
        // 动画开始前的UI效果
        [UIView animateWithDuration:0.34 animations:^{
            currentVC.view.frame = CGRectMake(0, HDScreenInfo.height, HDScreenInfo.width, HDScreenInfo.height);
        } completion:^(BOOL finished) {
            completion(finished);
        }];
    }
    @end
    

    这样的操作和模态视图出现和消失的视觉效果等效,同时保持了VCStack链

    [self.vcStack pushto:vc animation:[HDModelAnimation defaultAnimation]];
    [self.vcStack popWithAnimation:[HDModelAnimation defaultAnimation]];
    

    细节

    在自定义VCStack中设计到很多细节操作,这些操作的完善会让整个VCStack更加的健壮

    生命周期维护

    在VCStack中除了view的依赖的管理,同步操作还需要将对应的VC的生命周期管理起来,在日常的业务场景中这几个生命周期使用的频次是最高的

    • viewWillAppear
    • viewDidAppear
    • viewWillDisappear
    • viewDidDisappear
    • dealloc

    为了保持和系统生命周期的一致性,在push和pop操作中对VC的生命周期做了手动处理

    - (void)pushto:(UIViewController *)vc animation:(NSObject<HDVCStackAnimationProtocol> *)animation {
        // 添加手势处理
        [self panGestureWithView:vc];
        
        // 当前禁止任何手势
        [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
        [self.viewControllers addObject:vc];
        [vc viewWillAppear:false];
        [self.visibleViewController viewWillDisappear:false];
        [self.visibleViewController.view addSubview:vc.view];
        vc.vcStack = self;
    
        // 对底部的tabBar做层级操作
        if (vc.hdHideBottomBarWhenPushed) {
            // 这里什么都不做
            [self.tabBarManager.view bringSubviewToFront:vc.view];
        }
        
        if (animation) {
            // 动画开始
            [animation pushWithWillShowVC:vc currentVC:self.visibleViewController completion:^(BOOL finished) {
                if (finished) {
                    [self.visibleViewController viewDidDisappear:true];
                    [vc viewDidAppear:true];
                    self.visibleViewController = vc;
                    // 手势禁用关闭
                    [[UIApplication sharedApplication] endIgnoringInteractionEvents];
                }
            }];
        }
        else {
            // 手势禁用关闭
            [self.visibleViewController viewDidDisappear:false];
            [vc viewDidAppear:false];
            self.visibleViewController = vc;
            [[UIApplication sharedApplication] endIgnoringInteractionEvents];
        }
    }
    
    - (void)popToVC:(UIViewController *)popToVC
          animation:(NSObject<HDVCStackAnimationProtocol> *)animation
      willDismissVC:(UIViewController *)willDismissVC
    popCompleteHandle:(void (^)(BOOL))popCompletion {
        if (popToVC) {
            // 基础引用链
            willDismissVC.vcStack = nil;
            // 当前禁止任何手势
            [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
            if (animation) {
                [popToVC viewWillAppear:true];
                [willDismissVC viewWillDisappear:true];
                [animation popWithWillShowVC:popToVC currentVC:willDismissVC
                                  completion:^(BOOL finished) {
                                      if (finished) {
                                          [willDismissVC.view removeFromSuperview];
                                          [willDismissVC viewDidDisappear:true];
                                          [popToVC viewDidAppear:true];
                                          self.visibleViewController = popToVC;
                                          // 手势禁用关闭
                                          [[UIApplication sharedApplication] endIgnoringInteractionEvents];
                                          // completion handle
                                          if (popCompletion) {
                                              popCompletion(finished);
                                          }
                                      }
                                  }];
            }
            else {
                [popToVC viewWillAppear:false];
                [willDismissVC viewWillDisappear:false];
                [willDismissVC.view removeFromSuperview];
                [willDismissVC viewDidDisappear:false];
                [popToVC viewDidAppear:false];
                self.visibleViewController = popToVC;
                // 手势禁用关闭
                [[UIApplication sharedApplication] endIgnoringInteractionEvents];
                if (popCompletion) {
                    popCompletion(YES);
                }
            }
        }
        else {
            if (popCompletion) {
                popCompletion(NO);
            }
        }
    }
    

    对于dealloc 在持有链消失的时候能被系统检测到,可以正常的释放,当前的持有关系为:

    • VCStack持有数组
    • 数组持有VC
    • vc弱持有VCStack

    其中VC弱持有VCStack是为了兼容tabBarController的存在,如果工程是一个单一的VCStack完全可以用单例待提升实例。在pop的时候会主动解开所有的依赖

    VC.vcStack = nil 
    VCStack.array remove VC
    
    手势系统维护

    在每次push的时候,都会在View的层级上增加手势系统,当然这里也有协议的支持,如果VC实现了协议

    @protocol HDVCEnableDragBackProtocol <NSObject>
    - (BOOL)enableDrag;
    @end
    

    并标记为NO的时候,这个页面是不支持手势的。具体实现如下:

    - (void)pushto:(UIViewController *)vc animation:(NSObject<HDVCStackAnimationProtocol> *)animation {
        // 添加手势处理
        [self panGestureWithView:vc];
        .......
    }
    
    - (void)pangestureWithView:(UIView *)view completeHandle:(void (^)(void))completeHandle {
        UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
        self.successBlock = completeHandle;
        [view addGestureRecognizer:panGesture];
    }
    
    - (void)pan:(UIPanGestureRecognizer *)pan {
        // 当前正在拖动的view
        UIView *view = pan.view;
        // 即将要显示的View
        if (self.viewControllers.count > 1) {
            UIViewController *bottomViewController = self.viewControllers[self.viewControllers.count - 2];
            UIView *bottomView = bottomViewController.view;
            
            // 一些标记值
            static CGPoint startViewCenter;
            static CGPoint startBottomViewCenter;
            static BOOL continueFlag = YES;
            
            if (view && bottomView) {
                // 拖动开始的检测
                if (pan.state == UIGestureRecognizerStateBegan) {
                    // 拖动开始时View的frame需要先发生变化,保证和系统的UI风格统一
                    bottomView.frame = CGRectMake(- HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
                    view.frame = CGRectMake(HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
                    // 检测当前的拖动的位置是否在合适的点,当前确立,view的左边1/3z位置可以作为触发的初始点
                    CGPoint startPoint = [pan locationInView:view];
                    if (startPoint.x > (view.frame.size.width / 3.0)) {
                        continueFlag = NO;
                    }
                    else {
                        continueFlag = YES;
                        // 将底部的View遮罩,避免手势点击造成其他问题
                        [bottomView addSubview:self.maskView];
                    }
                    startViewCenter = view.center;
                    startBottomViewCenter = bottomView.center;
                }
                else if (pan.state == UIGestureRecognizerStateChanged) {
                    if (continueFlag) {
                        // 拿到对一个的偏移量
                        CGPoint transition = [pan translationInView:view];
                        view.center = CGPointMake(startViewCenter.x + transition.x / 3.0 * 2.0, startViewCenter.y);
                        bottomView.center = CGPointMake(startBottomViewCenter.x + transition.x / 3.0, startBottomViewCenter.y);
                    }
                }
                else if (pan.state == UIGestureRecognizerStateEnded) {
                    if (continueFlag) {
                        // 将遮罩view去除
                        if (self.maskView.superview != nil) {
                            [self.maskView removeFromSuperview];
                        }
                        // 开始收尾动画
                        if (view.center.x > (view.frame.size.width / 6.0 * 7.0)) {
                            if (self.successBlock) {
                                self.successBlock();
                            }
                        }
                        else {
                            // 禁止用户操作
                            [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
                            // 还原到初始的位置
                            [UIView animateWithDuration:0.34 animations:^{
                                view.frame = CGRectMake(HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
                                bottomView.frame = CGRectMake(- HDScreenInfo.width / 3.0, 0, HDScreenInfo.width, HDScreenInfo.height);
                            } completion:^(BOOL finished) {
                                if (finished) {
                                    // 解开用户手势操作
                                    [[UIApplication sharedApplication] endIgnoringInteractionEvents];
                                    // 还原对象的位置
                                    view.frame = CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.height);
                                    bottomView.frame = CGRectMake(0, 0, HDScreenInfo.width, HDScreenInfo.height);
                                }
                            }];
                        }
                    }
                }
            }
        }
    }
    
    动画期间手势隔离

    自定义VCStack提供了很多便捷的操作API,这些api中很多是伴有animation 操作的,为了避免用户在animation期间响应手势导致一些未知的错误,在代码段做了容错

    - (void)pushto:(UIViewController *)vc animation:(NSObject<HDVCStackAnimationProtocol> *)animation {
        // 添加手势处理
        [self panGestureWithView:vc];
        
        // 当前禁止任何手势
        [[UIApplication sharedApplication] beginIgnoringInteractionEvents];
       ........
        
        if (animation) {
            // 动画开始
            [animation pushWithWillShowVC:vc currentVC:self.visibleViewController completion:^(BOOL finished) {
                if (finished) {
                    .......
                    // 手势禁用关闭
                    [[UIApplication sharedApplication] endIgnoringInteractionEvents];
                }
            }];
        }
        else {
            // 手势禁用关闭
           .....
            [[UIApplication sharedApplication] endIgnoringInteractionEvents];
        }
    }
    
    // pop 也是同样的逻辑
    
    // 在右滑手势中增加了底部的bottomVC的遮罩,避免左滑手势响应其他事件带来问题
    if (pan.state == UIGestureRecognizerStateBegan) {
        .....
        else {
               continueFlag = YES;
               // 将底部的View遮罩,避免手势点击造成其他问题
               [bottomView addSubview:self.maskView];
               }
           .......
     }
    ......
    else if (pan.state == UIGestureRecognizerStateEnded) {
          if (continueFlag) {
              // 将遮罩view去除
              if (self.maskView.superview != nil) {
                 [self.maskView removeFromSuperview];
              }
      }
    

    总结

    在实现的过程中,一开始的实现是围绕着一个NavigationStack的方式去进行的,这在实际的开发中已经满足了大多需求,因为大多的app都是一个Navigation的方式管理的,即便底部存在多个业务窗口,但是在下一级页面都会关闭底部的这个入口。
    为了支持系统tabBar和VCStack混合管理的方式,在原来的基础上集成了tabBarManager+VCStack。是的整体的逻辑更靠近系统TabBar+navigation的管理方式。

    最后说一句项目还在完善中,如果有兴趣可以一并完善。项目地址如下
    VCStack
    VCStack+TabBarManager

    相关文章

      网友评论

          本文标题:自定义VCStack

          本文链接:https://www.haomeiwen.com/subject/rlvklqtx.html