真!一行代码集成0耦合QQ侧滑功能
1.为啥要重复造轮子
想要做这个侧滑功能是因为我们项目中有使用到侧滑的菜单,开始我们也没有使用另外一些比较出名的侧滑框架,因为在UI这部分个人不是很喜欢用第三方,总感觉有时候不太符合界面的自定义,而且每开发一个自己没做过的功能自己实现一次也是对自己的一种锻炼,我们当时app的侧滑,就是直接在window的左侧摆了一个控制器的view,根据事件对这个view和根控制器进行移动交互,之后也有看一些比较出名的侧滑框架,发现其实现原理都是类似的,耦合度非常高,框架替换成本也高,且每次打开UI层次解析界面的时候,整个window上面总是自带着这一坨隐藏在背后控制器的view,看上去有点儿不爽。例如下面这个图,刚启动程序就是这样的:
示例图@2x.png总感觉不是那么好,于是在结合之前有看到过的自定义的转场动画(UIViewControllerAnimatedTransitioning)脑袋里冒出一个想法,是否可以使用系统的push或者present通过自定义转场时候的动画来实现它呢,自己觉得可行,于是撸起袖子开始干!关于控制器的转场动画的基本学习,可以看看这篇文章 iOS7中的ViewController切换
2.未知标题
我们的优势:
- 整个框架没有任何限制与依赖,全程类似系统Push操作,你给我一个控制器,我还你一个侧滑抽屉效果,甚至你可以设置10个不同的侧滑抽屉。
- 对原有框架0污染0侵入,不需要设置什么LeftVC,rightVC,middleVC这些东西,也不需要继承自啥TabarController,直接使用0耦合!
- 当抽屉界面在关闭的情况下,抽屉界面安全释放,不会一直存在内存中,界面也不会一直藏在控制器下或者屏幕外。
先看一下目前我们可以实现的效果
示例图1.gifscrollView嵌套的场景
scrollView嵌套的场景.gif
我们在重复显示左边以及右边菜单之后UI的层级
111@2x.png正如,代码虐我千百遍,我待代码如初恋,不管你怎么弄,怎么操作,最后我还是原来纯洁的样子~
3.如何使用?
如果你想实现目前QQ这种侧滑(上图左按钮事件),我们的使用非常简单!!真正的一行代码,一毛一样骗人是小狗🐶~首先导入 #import "UIViewController+CWLateralSlide.h" 然后在需要显示左侧的控制器的时候调用cw_showDrawerViewController:方法:
// 导航栏左边按钮的点击事件
- (void)leftClick {
// 自己随心所欲创建的一个控制器
LeftViewController *vc = [[LeftViewController alloc] init];
// 调用这个方法
[self cw_showDrawerViewController:vc animationType:CWDrawerAnimationTypeDefault configuration:nil];
}
耦合度非常低,想侧滑出哪个控制器直接传值需要滑出来的VC就OK,不需要提前配置任何元素。任何地方都能调用,是不是so easy~
4.这个框架的实现。
实际上就是使用了系统的present方法,我们做的仅仅只是把present这个动画自定义了,简单说一下在写这个框架过程中需要注意几个坑,但是在这之前,强烈建议先看看如何自定义转场动画,不然会一脸懵逼。
首先如何自定义转场动画我们就不多说了,网上有非常多优秀的文章都有说到,可以自行搜索一下,比如这个iOS自定义转场详解03总共有4个demo,而且这里面有非常多经典的动画效果。有兴趣可以学习一波。。说几个在写功能时候的坑
a、按照流程,写好动画~但是在转场动画完成的时候,根控制器消失了!!!!
我们的根控制并没有使用截屏图放在后面用来做动画,而是直接将控制器的view放在动画容器containerView内(为啥不用更方便的截图来做动画,是因为仔细看过QQ的侧滑,发现在打开菜单的状态下,QQ右侧界面在接收到消息还是会跟着变,所以QQ不是用的截屏图来做动画的,于是,它能那样实现,咱们肯定也是能实现的对吧),在实现的过程中发现这样会导致在动画结束的时候,根控制器这边的view是消失,如下图:
示例图2.gif当时有点没想明白,看了一些网上大神的资料,有一些用截屏的图片做动画的不会有这个问题(但我们已经明确QQ不是用的截屏图,所以咱们肯定也可以用这个之外的方式解决是吧),还有一些是设置控制器的modalPresentationStyle为UIModalPresentationCustom,设置之后发现显示侧滑的时候正常了,哇,可行,以为OK了,但是当返回之后界面又消失了=。=!就像这样~
示例图3.gif
所以这样也不是特别好,最后我们从源头想了想,为什么我动画结束之后view会消失,无非就是几种情况,1、frame不对,2、透明度为0或者设置为hidden了,3、就是被从父视图移除了,显然1,2理论上都不会发生,最终我们在动画结束的时候打印根控制器view的父视图发现为nil,找到原因我们就解决它,所以我们在动画结束之后,又把该view添加到containerView上:
if (![transitionContext transitionWasCancelled]) {
[transitionContext completeTransition:YES];
// 动画完成,再次添加根视图的view
[containerView addSubview:fromVC.view];
maskView.userInteractionEnabled = YES;
}else {
[transitionContext completeTransition:NO];
[imageV removeFromSuperview];
}
这样就完全没问题了~具体为啥动画结束要被移除我也不是很明白,但是我猜可能是出于内存管理考虑,添加上去之后引用计数又+1不利于界面释放,所以动画结束又移除掉了,引用计数-1,让这个动画过程不对view的内存产生影响,知道为啥的童鞋可以留言指导一下。在苹果官方文档上completeTransition: 这个方法有说明:The default implementation of this method calls the animator object’s animationEnded(:) method to give it a chance to perform any last minute cleanup.(调用completeTransition: 之后 会默认调用animationEnded(:) 方法来进行清理工作,所以我们的猜测应该是正确的,动画完成就被清理了。。)
b、还有一个细节就是在做手势驱动结束的时候动画不连贯。
这个我们要先说一下在手势过程中,重要的几个方法
// 手势过程中,更新转场执行的进度,参数为所传的百分比
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
// 调用此方法会取消转场,并且回到转场动画之前的状态
- (void)cancelInteractiveTransition;
// 手势结束之后,如果想完成转场调用此方法
- (void)finishInteractiveTransition;
就是我们在手势过程中,会传一个百分比去调用第一个方法,然后一般会设置一个转场是取消还是完成的临界点,如果在手势过程中,超过这个临界点,我们就调用完成转场,如果不到这个临界点,我们就取消,比如我们临界点设置为转场完成的50%也就是0.5,那么在手势过程中超过0.5的时候我们会调用finishInteractiveTransition方法完成转场,但是转场动画的更新会从0.5突然跳到1.0,这时屏幕就会闪一下,甚至不仅仅是闪一下,还会弹几下再结束动画,就像这样:
示例图4.gif
在手势过程不到临界点值时调用取消动画的时候照样会出现这个问题。
提供一下我们的解决思路:
启动一个定时器,将未完成的动画用一个定时器一步步更新到动画结束后再调用完成转场或者取消转场,也就是说,比如我们现在手势拖动动画更新到0.5,这个时候我们松手,如果直接调用完成转场,界面就会从0.5猛的跳到1.0,中间的这段动画就会闪过去或者异常,于是我们将0.5-1.0这段时间的更新用一个定时器从0.5跑到1.0再调用完成手势切换,那么整个转场动画的过程就会变的非常流畅~我们定时器用的是CADisplayLink,实现方式如下:
- (void)startDisplayLink:(CGFloat)percent{
// 首先判断是完成转场动画还是取消转场动画
_toFinish = percent > 0.5;
// 再根据动画总时长求得剩下的归0(取消)或者完成转场动画需要执行的时长
CGFloat remainDuration = _toFinish ? self.duration * (1 - percent) : self.duration * percent;
// 以每秒60次刷新计算定时器需要执行多少次
_remaincount = 60 * remainDuration;
// 算出定时器每执行一次需要改变的动画百分比
_oncePercent = _toFinish ? (1 - percent) / _remaincount : percent / _remaincount;
// 开始定时器,每执行一次定时器,定时器剩余次数-1,当剩余0次时,结束定时器,完成转场。
[self starDisplayLink];
}
处理完这些,我们就获得了一个非常流畅的侧滑动画,很开心~
示例图5.gif
c、但是。。开心不过3秒。发现由于我们的侧滑出来的控制器实际上是present出来的,没有导航控制器!!!!那我们要在这个控制器里面进行push操作怎么办??一脸懵逼~
还好,咱们有QQ可以借鉴一下,因为我们大胆的猜测QQ就是present的,所以为啥QQ侧滑的控制器可以push呢,在使用中发现,QQ在push的时候应该是先把侧边控制器dismiss,然后再使用根控制器tabbarController的第一个控制器的导航控制器进行push的,也就是说分为两步,第一步把左侧present的控制器dismiss,然后拿到QQ的消息控制器的导航控制器进行push操作,为啥会这么觉得呢,因为使用QQ push出来的控制器再返回pop的时候是直接回到根控制器的!于是我们的push操作这样去实现它:
- (void)cw_pushViewController:(UIViewController *)viewController{
UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
UINavigationController *nav;
if ([rootVC isKindOfClass:[UITabBarController class]]) {
UITabBarController *tabbar = (UITabBarController *)rootVC;
NSInteger index = tabbar.selectedIndex;
nav = tabbar.childViewControllers[index];
}else if ([rootVC isKindOfClass:[UINavigationController class]]) {
nav = (UINavigationController *)rootVC;
}else if ([rootVC isKindOfClass:[UIViewController class]]) {
NSLog(@"This no UINavigationController...");
return;
}
[self dismissViewControllerAnimated:YES completion:nil];
[nav pushViewController:viewController animated:NO];
}
自己定义一个方法,最终目的就是找到一个最优的导航控制器来进行push操作,所以在使用这个框架的时候,侧滑出来的控制器要进行push操作,不能使用系统的push方法(毕竟它没有导航控制器,就算你打死它它也还是不能push呀),必须使用我写的这个方法,实现之后push效果如下:
示例图6.gif这些细节都处理之后,整个效果基本都没问题了~所以我们可以开心的去使用这个框架啦~
5.最后
第一次在简书上写文章,觉得对你有所帮助的童鞋帮忙点个喜欢(多谢🙏),想使用这个框架或者想看一下实现原理的童鞋前往我的github地址(很简单的啦):
走过路过star不要错过~感谢。
目前已经支持cocoapods安装。PS:第一次做支持cocoapods,真是一把心酸一把泪。。成功率在10%不到,加上网速又慢,真是极大的锻炼了我的耐心。能成功搜索到之后真是泪牛满面。如果遇到什么问题或者优化建议欢迎留言。。毕竟这是我的第!一!次!!!要践踏,请温柔~~。