响应链API和手势识别器的混合编程

这篇是总结上一周在开发中遇到的问题,以及针对这个问题又进一步做了研究后得出的结论和经验。一来是帮助自己深入理解UIKit的工作机制,形成自己的体系风格;二来是尽量帮助大家避过我踩过的坑.

抛出问题

目前我正在做照片编辑器的软件(photo collage), 简单来说就是让用户编辑,组合自己的照片,然后上传到walgreens的服务器进行打印,之后用户去店里去取。和国内美图秀秀这类软件还是有一些区别。既然是照片编辑,那么旋转,缩放,平移这些对相片基本的操作就必不可少,但也是这些看上去不起眼的功能往往会给你带来不小的困扰. 问题背景如下:

Collage View

如图所示,这是App的 "Editing View" 我只把中间的主要部分重画了下,用来阐述问题。灰色部分是最外层的container view, 姑且叫它 “root container view”(此环境下); A, B, C各自代表一个container view,并且在每个container view中,有一个imageView,我们可以对这个imageView进行一些基本操作,旋转,平移,缩放等。所以,在imageView上,就被添加了3个gesture recognizers。OK, 问题到目前为止还是很清晰的,好,我们接着往下走。在A, B, C这3个container上,也加了一个gesture, 叫“longpress gesture”用来处理用户长按操作,这个操作是用作交换A,B,C三个container view位置的。当用户长按某个container view,触发了longPressure操作后就可以进行拖拽以交换位置。

啰嗦了这么多把背景讲完,下面说下问题。老板突然要求要加入3D force touch,以便outstanding。本身加入force touch是一个很简单的工作(quick actions, peek and pop, 以及新属性应用), 但是背靠万恶的“legacy code”,添加新功能引入bug是不可避免的了。老板的要求是使用3D force touch触发之前longpress的操作,听上去很简单,只是把触发操作改了嘛其他的不动就好了,最多extract几个函数出来嘛,此时我的心情是:

开始解决问题

这里讲述我解决问题时的心路历程,如果您着急解决问题可直接跳过这部分,到第三部分。

OK, 那就让我们来看下怎么触发3D force touch。 在iOS9中, Apple为UITouch增加了2个新的属性: forcemaximumPossibleForceforce代表的就是你的按压力度,通常的默认值是1,由系统决定,被当做一个average touch。其真实值是0~6.66666...., 很多6....., 有些国外的网友测试过,6.66666....就是385克,这也是**maximumPossibleForce****的值. Apple提供范围如此之大的上下限就是让开发者可以积极利用force这个属性,开发出精准交互的App。

Ok,那我们在哪里调用这个touch对象呢?毫无疑问,我们首先想到的就是Respond Chain API, 那一系列的touchXXX方法。force touch是一个持续的过程,所以我们并不在touchBegan:withEvent中做force的检测,而是在touchMoved:withEvent中判断force的变化是否达到我们所设置的阈值. 当然,UIResponder中还有一系列pressXXX方法,例如pressesBegan:withEvent, 但这些是给physical button点击时使用的,例如Apple TV的遥控器等,对于普通的surface touch,还是touchXXX方法响应。

这时候,问题来了,在响应链API中,我们获取到的touch对象上的view是最底层的那个view,也就是说,当我们长按container view触发移动操作时,我们拿到的touch.view实际上是最下面的imageView, 而不是对应的container view。这会带来什么问题呢?因为image view本身就绑定了3个gesture,这些gesture本身就能响应用户的操作,而现在新加入的touchXXX系列方法也同样响应UITouch事件,处理不好就会有冲突,甚至崩溃,这也是为什么我本篇题目定位为混合编程的原因。

我首先想到的解决办法就是将所选imageView对应的container view找出来,之后再container view上加效果,做移动。但马上发现其实这样做是不可取的,因为touch.view返回值就是imageView,这个在整个touch事件周期中是不会改变的,而且在touchXXX API中,你能移动的必须是,也只能是一开始选中的imageView。那好,那我们可以做个测试,我把imageView提取了出来,加到了root container中,来做移动,这时候,imageView的确可以自由移动了,但是当你释放手指的时候,touchEnded:withEvent方法并没有被调用, 导致结束函数无法被触发。具体的原因我猜测是因为imageView被我强行加入到了顶层container,破坏了hitTest链,也就是响应链信息被破坏,导致touchEnded:withEvent无法被调用,没有对象触发它。当我们移走imageView,再在其对应的空白container view上点击下的话,touchEnded:withEvent会被调用。

这时候,我的心情....

真正解决问题

下了班和朋友讨论了下,他提供了一个折衷的方案(又印证了经验丰富的老司机感觉不会错的观点---我说的): 使用touchMoved:withEvent来检测force touch,触发操作; 之后使用UIPangestureRecogonizer对象进行之后的所有操作. 我听完后整个人就开朗了~~~~

第二天,我要把这个theory付诸实际,开始调试代码,在touchMoved:withEvent:加入force touch的触发判断,之后做一个标记(boolean值)记录force touch已被触发。OK, 开始运行程序,长按后,的确触发了force touch但是当我拖拽的时候,对应的container view并没有移动,这就奇怪了,pangesture recogonizer已经加到了每个container view上面啊,为什么不触发呢?思前想后,八成又是响应链API和手势识别混用的后果,但Apple应该也给出了控制它们响应的方法。所以我打开了UIGestureRecognizer的头文件,发现在UIGestureRecognizerDelegate中有一个方法:gestureRecognizerShouldBegin,该方法控制着gesture是否该响应的返回值, 而我之前的拖拽无响应是不是和这个delegate method有关呢?我在当前的view contrller实现了这个方法,果不其然,当返回为YES时,panGesture顺利工作了!但是,container view里面的imageView也跟着滑动,这个是不需要的。所以,当触发force touch时,让imageView的gestureRecognizerShouldBegin:返回空即可(可以暴露imageView一个public attr给外部类,当触发force touch时, 将这个attr赋值为YES, 至于gestureRecognizerShouldBegin:, 系统在每次用户滑动时都会触发,所以你只需在该函数开头加入正确的判断即可,即手势类型判断和标记为判断)。

OK,既然说到了gestureRecognizerShouldBegin:我们就再深入研究下它的调用。该方法的调用可不是在实现该方法的类里面调用一次那么简单。这样说可能还有些晦涩,我们来举个栗子:

Example2

图中有3中颜色, 分别代表3个不同大小的view,小的view是大的view的subview。在每个view上,我都加上了tap gesture recognizer,并且在每个类里面,都实现了gestureRecognizerShouldBegin:. 运行程序后,我们在最内层的蓝色view上点击下,程序立刻在蓝色view的gestureRecognizerShouldBegin:停下(打了断点), 但这时候,tap gesture所持有的view却不是蓝色的view,而是橙色和外层绿色或者蓝色中的某一个(随机的,我测试过10次以上了...); OK, 为了之后方便讲解,我们假定tap gesture第一次点击持有的view就是蓝色view,当我们点击继续后(声明: 3个view的gestureRecognizerShouldBegin:均返回YES),橙色view的gestureRecognizerShouldBegin:被调用了,点击继续后,又跳回蓝色view的gestureRecognizerShouldBegin:方法中,再点击继续,则跳到绿色view的gestureRecognizerShouldBegin:方法。综上来说,最底层的subview的gestureRecognizerShouldBegin:方法被调用了3次, 有且只有最底层的view的gestureRecognizerShouldBegin:返回为YES时,上层的gestureRecognizerShouldBegin:才会被调用。这种机制会带来什么后果或者说bug呢? 如果你在最底层的subview中的gestureRecognizerShouldBegin:方法判断错误的话, 一旦其放回NO, 上层的gesture都不会被触发,手势识别全部不会响应.听上去好严重的样子, 那有没其他方法可以代替呢?有的,那就是shouldReceiveTouch:这个方法, 这个方法会在touchBegan:withEvent: 之前调用, 目的在于告诉gesture recogonizer是否应该接收touch事件,如果返回NO,则不接收。这个方法在每个view只会调用一次,不存在底层subview调用多次的情况。

UIGestureRecognizerDelegate还有其他的函数,但都比较简单,使用场景比较单一大家可以自行看下.

好,回到正题。解决了触发pan gesture的问题之后,新的问题来了。当用户仅触发了force touch并不移动时, 系统不会发送touchEnded:withEvent:回来, 导致收尾方法无法被调用,造成用户体验上的缺失.那原因是什么的?既然是gesture recogonizer出了问题,那就去它的头文件看看。我发现了2个比较有意思的属性: delaysTouchesBegandelaysTouchesEndeddelaysTouchesBegan是用来告诉响应链你的touchBegan:withEvent:别调用,一切让我的gesture识别器来此处理,这个属性默认值是NO; delaysTouchesEnded这个属性默认值是YES,告诉响应链你的touchEnded:withEvent:别管,我gesture识别器来做。这个属性就是造成之前提到bug的罪魁祸首, 在往view上天剑gesture时,将此属性置为NO即可。当然那你也不用担心,当触发pan gesture时,结束函数就是gesture控制的,不会在调用touchEnded:withEvent:方法了。

总结

好了,到此为止,问题解决了。让我对响应链和gesture识别器又多了一层的了解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,227评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,755评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,899评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,257评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,617评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,757评论 1 221
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,982评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,715评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,454评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,666评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,148评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,512评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,156评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,112评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,896评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,809评论 2 279
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,691评论 2 272

推荐阅读更多精彩内容