自定义VCStack

2018-12-27  本文已影响1人  chieryw

背景介绍

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

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

系统VCStack存在的困境

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

上面几个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这种方式也是存在问题的,先看一下抽象层面的信息:

关键点出在了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参数也是三个

但是这里的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的生命周期管理起来,在日常的业务场景中这几个生命周期使用的频次是最高的

为了保持和系统生命周期的一致性,在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 在持有链消失的时候能被系统检测到,可以正常的释放,当前的持有关系为:

其中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

上一篇下一篇

猜你喜欢

热点阅读