美文网首页iOS工作系列学无止境tom
轻松学习之二——iOS利用Runtime自定义控制器POP手势动

轻松学习之二——iOS利用Runtime自定义控制器POP手势动

作者: J_雨 | 来源:发表于2015-03-31 13:44 被阅读73906次

    前言

    苹果在IOS7以后给导航控制器增加了一个Pop的手势,只要手指在屏幕边缘滑动,当前的控制器的视图就会跟随你的手指移动,当用户松手后,系统会判断手指拖动出来的大小来决定是否要执行控制器的Pop操作。

    nav_pop_origin.gif

    这个操作的想法非常好,但是系统给我们规定的范围必须是屏幕左侧边缘才可以触发,这样实际使用过程中对于有些产品会产生不便,于是有些app就采取整个屏幕都响应这个手势并且pop动画还是用系统原生的,这样操作起来确实方便好多。

    nav_pop_custom.gif

    开始大家一定会有疑问,给控制器的View加个手势然后拖动控制器的View时改变它的frame不就可以了吗?没错,加手势这个想法是正确的。但是,由我们自己来改变控制器视图的位置是比较麻烦的,细心的朋友一定发现了,我们自定义pop手势上面的导航栏也是在随着你的手势拖拽而变动的,所以这样做还需要负责导航栏的动画,而且有一个重点问题,如果单独拖动view,这个view下面会是黑黑的一片,因为控制器的push和pop层级是由系统管理的。

    nav_pop_failed.gif

    所以走这条路虽然可以,但实现起来会比较艰辛。那么,如何实现这个效果呢?今天就给大家提供两套实现方案。


    [1]

    方案一:自定义UIViewControllerInteractiveTransitioning对象,实现导航控制器代理方法。

    这个是苹果官方推荐的做法,在WWDC 2013 218 - Custom Transitions Using View Controllers中有说明。

    这套方案虽然实现比较麻烦,但是动画相对灵活,你可以实现这样的效果,

    nav_pop_cube.gif

    也可以有这种效果。

    nav_pop_flip.gif

    其实这个拖动过程属于导航控制器的动画,所以我们需要重写UINavigationController的两个代理方法,navigationController:animationControllerForOperation:fromViewController:toViewController:(名字很长下面就称为方法1)和
    navigationController:interactionControllerForAnimationController:(方法2)。
    解释一下他们的作用,方法1是苹果提供给我们用来重写控制器之间转场动画的(pop或者push)。方法2你可以这样理解,苹果让我们返回一个交互的对象,用来实时管理控制器之间转场动画的完成度,通过它我们可以让控制器的转场动画与用户交互(注意一点,如果方法1返回是nil,方法2是不会调用的,也就是说,只有我们自定义的动画才可以与控制器交互)。

    下面我们来看一下实现过程。为了便于大家理解,我会尽量在Demo中的注释写的最清晰明了。
    同时,我们先用最简单的代码实现,在这篇文章的最后我会对本例中的Demo提供一个相对合理的写法。

    首先在方法1中,我们返回一个遵守了UIViewControllerAnimatedTransitioning协议的对象,它就是自定义的动画对象,我们给它起名PopAnimation,在这个类中实现两个方法来自定义转场动画。

    屏幕快照 2015-03-28 下午6.49.05.png

    再来看方法2,我们需要返回一个遵守了UIViewControllerInteractiveTransitioning协议的对象(提示一下,这两个协议容易混淆,要注意区分,一个是负责动画,一个是负责交互过程),苹果已经有一个类专门处理这个功能,它叫UIPercentDrivenInteractiveTransition,当然你也可以自定义一个这样的类。我们可以这样理解它的作用:前面在方法1中返回的动画,会在执行的过程中被系统分解以用于用户交互,这个交互过程的动画完成度就由它来调控。下面我们来看一下如何使用它。(为了让控制器视图拖动,我们给控制器的视图加了一个拖动手势,在拖动方法里我们对这个对象进行操作)

    屏幕快照 2015-03-29 下午12.33.59.png

    最后在视图控制器里重写导航栏的两个方法。


    屏幕快照 2015-03-29 下午12.37.51.png

    有两点不要忘记:

    1. 设置导航控制器的代理为当前控制器。
    2. 给控制器加手势。

    OK,这样我们就完成了这个过程。

    nav_pop_own.gif

    [2]

    方案二:Runtime+KVC

    要了解这样的做法,需要有Runtime的一些知识,会涉及到私有变量、私有方法的获取,但是这样做比较简单也比较有趣,如果你感兴趣就继续看下去吧。关于Runtime的知识,今后我会分享到博客里,朋友们敬请期待。

    为了方便大家阅读下面的代码,我们需要先了解系统的这个手势。

    前面我们了解到,这个手势属于UINavigationController,我们就跳到它的头文件里看看能不能找到线索。这个思路是正确的,确实有一个手势叫做interactivePopGestureRecognizer。属性为readonly,就是说我们不能给他换成自定义的手势,但是可以设置enable=NO。ok,既然找到了它,就打印一下看看它到底是一个什么手势。

    屏幕快照 2015-03-26 下午5.17.35.png

    通过log,我们看到他属于UIScreenEdgePanGestureRecognizer这个类(之前我是没有用到过),它继承自UIPanGestureRecognizer,出现在IOS7以后,是专门处理在屏幕边缘触发的手势类型,并且只有一个属性叫edges,用来设置它的触发边缘(上、下、左、右、全部)。看到这里一些朋友会想,直接改它的edges为全部可不可以?经过试验了解到,改这个属性是没用的,它只能用来触发边缘,设为全部的意思是四个方向的边缘会触发,而且用来做控制器POP手势的只有左边缘。

    我们继续看它的log。控制台除了打印了它的类,还打印了它的触发target:_UINavigationInteractiveTransition(这是一个私有类,看来是专门用来做导航控制器交互动画的),和action:handleNavigationTransition(这是它的一个私有方法),我们要做的就是新建一个UIPanGestureRecognizer,让它的触发和系统的这个手势相同,这就需要利用runtime获取系统手势的target和action。

    那么如何获取这个target呢?一开始我用kvc想直接获取这个手势的target,程序崩溃了,原来它根本没有这样一个属性。所以我能想到的是,先利用runtime遍历它的所有成员变量,看看系统是怎么存储这个属性的,


    屏幕快照 2015-03-29 下午3.25.02.png

    通过log我们可以看到,UIGestureRecognizer有一个叫_targets的属性,它的类型为NSMutableArray。


    屏幕快照 2015-03-29 下午3.25.09.png
    它是用数组来存储每一个target-action,所以可以动态的增加手势触发对象。那么又是什么存储每一个target-action呢?为了了解这个我们拿到这个属性的名字"_targets"通过kvc获取它,接着打印出来。
    屏幕快照 2015-03-29 下午3.33.54.png
    屏幕快照 2015-03-29 下午3.34.01.png

    可以看到,由于系统重写了它的description方法,所以我们没办法通过打印获取这个对象是什么类型。既然不能打印,那么我们就用断点调试,来看它的真实类型,

    屏幕快照 2015-03-29 下午3.37.32.png

    我们看到,原来每一个target-action是用UIGestureRecognizerTarget这样一个类来存储的,它也是一个私有类。
    苹果把许多的类做私有化也是有原因所在,其实在平时我们拿到这个类也是没有用的,他们的目的之一是避免对开发者公开无用的类,影响了封装性。所以在类的设计上,还是要向苹果学习。

    下面直接看代码。

    我们在控制器的ViewDidLoad加上这段代码,并且它只需要执行一次。


    屏幕快照 2015-03-29 下午4.07.48.png

    优化

    这个demo我会提供给大家,下面简单说下程序的优化思路。

    • 优化点一:对于方案一,其实不应该把导航控制器的代理方法以及手势处理的方法交给视图控制器,因为这段代码不是属于某一个视图控制器,而是全局的导航控制器,所以我们应该参考苹果的设计思想:新建一个专门管理交互过程的对象,这个类我们叫做NavigationInteractiveTransition。

    • 优化点二:再来看之前的ViewDidLoad中只执行一次的代码,其实写在这里也不够妥当,同样的,这段代码也不属于某一个Controller,优化方案是新建一个导航控制器,在这个导航控制器的viewDidLoad中写上这些代码,这样也并不需要dispatch once。

    • 优化点三:由于我们自定义的手势是加在一个私有view上,这个view是一个全局的,所以当这个控制器为根控制器时,我们的手势还是在起作用,这就相当于对根控制器做了pop操作,这会出现一个错误nested pop animation can result in corrupted navigation bar。导致这个错误的原因还有一个,如果我们pop的动画正在执行,再去触发一次手势,会导致导航控制器和导航条的动画混乱。为了避免问题出现我们需要成为手势的代理,判断当前控制器是否为根控制器并且pop或者push动画是否在执行(这个变量是私有的,需要用kvc来获取)。


      屏幕快照 2015-03-30 下午5.06.24.png

    经过最后的优化,视图控制器可以什么都不写,想使用这个效果,只要使用我们自定义的导航控制器就可以了,这样的好处是手势动画与控制器完全解耦,并且不用给每一个控制器都addGesture。


    给大家推荐一个仓库https://github.com/nst/iOS-Runtime-Headers,这个仓库可以调取苹果的所有私有方法头文件,相当强大。

    最后放上这个demo的地址:https://github.com/zys456465111/CustomPopAnimation(使用时,切换工程的scheme就能切换不同方案。对于方案二,只需要导航控制器的类就可以了。)

    感谢大家,轻松学习系列还会继续下去,我会尽量写出更多通俗易懂的文章,让开发变得轻松起来,我的微博:http://weibo.com/JazysYu


    1. 方案一。

    2. 方案二。

    相关文章

      网友评论

      • 番茄鸡蛋汤圆:楼主~请教个问题,为啥我用runtime打印UINavigationBar的私有属性时并没有“_backgroundView”这个属性啊,但是用KVC却能取到
        番茄鸡蛋汤圆:补充一下,我是在iOS11上打印的。我又在iOS10+上试了,结果里有的是这个属性 “_barBackgroundView : @"_UIBarBackground" ”,仍然没有 “_backgroundView”这个属性。但是以上两个情况用KVC取的时候,都能正确取到。这是为什么啊
      • 西风颂:demo中方案一的两种动画怎么运行的有问题呢?
      • Echo126:作者你好,你的方案二的demo给了我很大帮助。但是我遇到了一点问题,解决不了,想请教你一下。
        我参考你的demo放到了我们项目里,打印了一下_targets ,如下
        <__NSArrayM 0x604000854ca0>(
        (action=wrapperAction, target=<BLBlockWrapper 0x60400064a620>)
        )
        正常应该是<__NSArrayM 0x1c0249ff0>(
        (action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 0x100713c60>)
        )这样的,所以我写的侧滑不好使。我自己参考你的方法写的demo里打印的是正确的,但是放到项目里就是不对。项目里有很多的第三方sdk,我猜测有可能是第三方的问题。作者你见过这种情况吗?或者见过BLBlockWrapper是什么吗?请给些建议,谢谢啦!
      • slowdony:楼主,请问一下,如果在UINavigationController设置了手势返回,但是想在其中一个子控制器里吧这个手势取消掉,如何取消,我在子控制器里在gestureRecognizerShouldBegin这个代理方法里返回no,还是没有取消
        J_雨:确保没有其它地方打开。在正确时机设置比如:viewDidAppear。设置以后debug下看看
        slowdony:@J_雨 我在自控制器中这样设置 self.navigationController.interactivePopGestureRecognizer.enabled=NO;,还是可以滑动:joy:
        J_雨:@SlowDony 用navigationCon.interactiveGesture....enabled来控制
      • 妹子爱编程:小哥哥棒棒哒
      • PGOne爱吃饺子:楼主你好,文中的这句话 “那么如何获取这个target呢?一开始我用kvc想直接获取这个手势的target,程序崩溃了” ,有点不明白,为什么要获取target呢
      • Sunsgne丶D:当前页面导航栏隐藏的时候,push到下一个页面,从下一个页面侧滑返回过程中,导航栏都变成白色了
        FengxinLi:我也是请问下解决没有?
      • FB不是非死不可:我想问一下,右滑距离不够的情况下,页面不会pop,但是此时存在最左边的屏幕黑屏的情况,没找到原因
      • 遥想月下:厉害了!
      • Cherish183:方案二 ,其实不用runtime也是可行的,通过 self.interactivePopGestureRecognizer.delegate 就可以拿到target 和action了。
      • brownfeng:思路超级棒
      • bf1047220875:私有api能成功上架吗
      • wuqh1993:讲的很好,学习了! 想请教您一个问题:您有一段代码 是 [self valueForKey:@"_isTransitioning"],用valueForKey拿到这个是否正在做动画的这个私有属性。您是如何知道 这个私有属性的名字是叫 isTransitioning 的?
      • 松树李树:大神,我这项目前段时间还好好的,怎么突然这个全屏热滑就不能用了呢???
      • UItachi:你的UITabBarController+ZFPlayerRotation UINavigationController+ZFPlayerRotation这两个Category 写得真是一坨狗屎,Category不是用来重写原类中已有方法的,这是禁忌,特别是对于library developer来说。详情我再写一个issue。
        UItachi:https://github.com/renzifeng/ZFPlayer/issues/150
      • ForgetThatNight:请问怎么解决和左滑删除cell的兼容问题,有右滑返回的时候,左滑删除就失效了,设置手势的优先级也没有用
      • 多鱼影视界:请问QQSlideMenu 这个Demo 如何在主界面添加 leftBarButtonItem
      • 钱塘老酒酿:最近产品要求解决导航条按钮点击反应迟钝问题,找了很久的原因 发现去掉FDFullscreenPopGesture 这个东西,反应就灵敏多了!是不是因为iOS10 导致变得卡顿啊~导航按钮 都是UIButton 赋作 UIBarButtonItem , 在这里留言望指导。
      • iYeso:好点子我喜欢
      • 多鱼影视界:你好 请问一下简书中如何实现页内跳转呢
        我用MarkDown语法 错误
        1. 先定义一个锚(id)

        <span id="jump">Hello World</span>

        2. 然后使用markdown的语法:

        [XXXX](#jump)
        这样用的
      • 343cea0f7107:6666666
      • liangdahong: :grin: 仔仔细细的看了一下,感觉太神奇了,感觉分析问题的思路一流。 :sob:
      • 1552e4eccec5:如果可以的,可以教一下iOS-Runtime-Headers,怎么使用吗,下载下来以后不知道如何倒入。官方没有DEMO 只有一个小例子
      • qBryant:学习了!
      • 8f4fd00eb5a5:我的天呢!楼主霸气牛X!第二个方法真的好赞!我看孙源大神的forkingdog还写了一个AOP的形式!把我们养的越来越懒了 :joy: 还是谢谢楼主的分享
      • 960d2a7a31bd:在pop动画一和二里,如果我的拖动一点的话,那么就会看到红色的View闪烁,这个bug不知道该怎么解决...如果不加pop动画的话,没有问题
        960d2a7a31bd:@ivan_x 没有..代码尚未研究明白,
        a6fdec1a1c8b:@渐行渐远渐无殊 同问 已经解决了吗
      • 960d2a7a31bd:为什么我在代码中好像没有看到有关runtime的方法 :cry:
      • 960d2a7a31bd:特别从cocoachina上过来的.很棒!
      • 烟雨痕:@J_雨 Hi, 请问我右划的时候, 返回有tabBar界面, 但是我左划时会出现tabBar, 请问如何处理? 就是只有右划才会返回, 我尝试多次修改了, 但是都没有成功, 所以希望帮忙解决一下.
      • John_LS:请教个问题,我在页面中有个视频播放,这样自定义的pop动画会阻碍播放,请问有解决方法吗
      • 王仁洁:那假如我某个(些)界面不想有这个向右滑动返回手势(即只能点击左上角popView)该怎么禁用呢?
      • 汤昊:好!!!
      • 汤昊:好!!!
      • XDC:第二种方法,如果是非控制器可以返回,但是如果是跟控制器,会出现假死情况,怎么解决啊?

      本文标题:轻松学习之二——iOS利用Runtime自定义控制器POP手势动

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