iOS 自定义VCStack

语言: CN / TW / HK

本文作者:汪汉东

背景介绍

最近开发的几个工程使用的都是系统的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)]

VCStack-01.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,使得所有实例的调度一致。结构如下:

VCStack-02.png

下面是实例的生成api,在实际的使用中每个独具特色的动画协议都是这么写的,他们的具体实现放在了集成的协议中 @interface HDVCStackAnimation : NSObject <HDVCStackAnimationProtocol> + (instancetype)defaultAnimation; @end 协议本身和堆栈的逻辑保持一致 ``` @protocol HDVCStackAnimationProtocol - (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的处理,来让这个逻辑更加健壮。同样的交叉影响的存在也被开发人员决定,只有设计中存在这种交叉影响,才会在使用中存在这样的逻辑。设计的节点已经被开发人员管控,需不需要这种逻辑交互已经不再是一个黑盒

VCStack-03.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 *)animation;

/** 出栈操作

@param animation 出栈动画 / - (void)popWithAnimation:(NSObject )animation;

/** 出栈到根节点操作

@param animation 出栈动画类型 / - (void)popToRootViewControllerWithAnimation:(NSObject )animation;

/** 出栈到指定的vc操作,匹配条件是当前的vc名称

@param vcName 即将要显示的vc名称 @param popAnimation 出栈动画 / - (void)popToVCWithName:(NSString )vcName animation:(NSObject *)popAnimation;

/** 出栈到指定的vc,匹配条件是实例对象的id指针是否相等

@param vc 即将要显示的vc实例 @param popAnimation 出栈动画 @param popCompletion 操作完成之后的回调,主要用于pop then push这种操作 / - (void)popTo:(UIViewController )vc animation:(NSObject *)popAnimation popCompleteHandle:(void (^)(BOOL))popCompletion;

/** 出栈到指定的vc名称,之后再压栈到一个的vc

@param popVCName 即将在栈顶出现的vc名称 @param popAnimation 出栈动画 @param pushVC 即将压栈的vc实例 @param pushAnimation 压栈动画 / - (void)popToVCWithName:(NSString )popVCName animation:(NSObject )popAnimation thenPushTo:(UIViewController )pushVC animation:(NSObject *)pushAnimation;

/** 出栈到指定的vc实例,之后再压栈到一个的vc

@param popVC 即将在栈顶出现的vc名称 @param popAnimation 出栈动画 @param pushVC 即将压栈的vc实例 @param pushAnimation 压栈动画 / - (void)popTo:(UIViewController )popVC animation:(NSObject )popAnimation thenPushTo:(UIViewController )pushVC animation:(NSObject *)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 )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 )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 )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 )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