ForceTapGestureRecognizerはForceTouchGestureRecognizerに名前を変更しました。
発端はSloppySwiperがiPhone6sで高頻度で動作しなくなりました。これはシミュレータでは発生せずどうやらiPhone6sのみ(6s+では検証できてませんがおそらく同等)で起きる現象でした。iOS9のiPhone4sでは発生しないのでiOS9.xの問題でもなさそうです。
SloppySwiperはUINavigationController
で利用可能な左エッジスワイプでpopするのを画面内のどこからでも利用できるようにした、Interactive Transitions
のアニメーターです。
本来だったら下のgifの様に右方向にドラッグするとInteractive Transitions
が開始され1つ前のViewControllerが表示されるのですが、これがiPhone6sだと ドラッグからのInteractive Transitions
が高頻度で動作しない という不具合です。
SloppySwiperのREADME.mdから引用
調査したところ問題の箇所は、
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
// 省略
CGPoint velocity = [self velocityInView:self.view];
// 省略
}
https://github.com/fastred/SloppySwiper/blob/master/Classes/SSWDirectionalPanGestureRecognizer.m#L22
UIPanGestureRecognizer
をサブクラス化してるSSWDirectionalPanGestureRecognizer
で-touchesMoved:withEvent:
内で使用してる-velocityInView:
がCGPointZeroを返すことが原因でした。
Force Touchを持つデバイスでは-touchesMoved:withEvent:
の呼び出しタイミングが異なる
iPhone4sとiPhone6sで画面をタップする(指を動かさない)とそれぞれ以下が呼びだされます。
iPhone4s
-touchesBegan:withEvent:
iPhone6s
-touchesBegan:withEvent:
-touchesMoved:withEvent:
iPhonw6sだと指を動かしていないのに-touchesMoved:withEvent:
が呼ばれています。そこで今回の問題の箇所である-velocityInView:
は指を動かしていないためCGPointZeroが返ってくるというわけです。その後iPhone6sでも指を動かせば正常に-velocityInView:
が値を返してくれました。
結論: Force TouchのZ軸方向への移動は-touchesMoved:withEvent:
を呼びだす
iPhone6sで何が起きているかを調べてみたところ、このx,y軸方向への移動量ゼロの-touchesMoved:withEvent:
で変化が起きていたのは- [UITouch force]の値でした。
forceはiPhone6s(6s+)で追加されたForce Touch(3D Touch)の強く押し込んでいるときの圧力量です。
結論としてはForce Touch(3D Touch)搭載の機種ではx, y軸方向への移動以外にz軸方向への移動でも-touchesMoved:withEvent:
が呼び出されるということです。たしかに移動という観点では正しい挙動だと納得できます。
稀に正常に動作していたのは初回の-touchesMoved:withEvent:
でz軸方向への移動に加えてx, y軸方向への移動があった場合で、たまたま従来の処理方法でうまく動作していただけでした。
そういえば
これに気づく数時間前にアプリ内でForce Touchを利用しやすくするためにGestureRecognizer化したForceTouchGestureRecognizerを作っていました。
これの実装も-touchesMoved:withEvent:で検知していましたね・・・うっかり忘れてた・・・。
ForceTouchGestureRecognizerはUIGestureRecognizer
のサブクラスなので、
[self.forceTapGestureRecognizer addTarget:self action:@selector(viewForceTapped:)];
[self.singleTapGestureRecognizer addTarget:self action:@selector(viewSingleTapped:)];
[self.doubleTapGestureRecognizer addTarget:self action:@selector(viewDoubleTapped:)];
[self.longPressGestureRecognizer addTarget:self action:@selector(viewlongPressed:)];
のように他のUIGestureRecognizer
との共用も楽にできます。
上のgifはForce TouchとUITapGestureRecognizerの繰り返しですが、ExampleではForce Touchに加えてシングルタップ/ダブルタップ/ロングプレスとの共用方法を実装しています。
アプリ内で利用可能なForce Touchを使いたい方はぜひお試しください!
追記: UIResponderでの対応
他にもデバッグを進めていたら-[UIResponder touchesMoved:withEvent:]
も同様の挙動でした。
こちらは次の方法でx, y軸方向への移動があるかを判定して対応しました。
- (void)touchesMoved:(NSSet *)touches
withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
if (CGPointEqualToPoint([touch locationInView:self], [touch previousLocationInView:self])) {
// x,y軸方向への移動がないForce Touchによるz軸方向への移動
// ここで-[UITouch force]を利用してForce Touch時の挙動にも対応できる。
return ;
}
// x, y軸方向への移動
}
また当初の問題だったSloppySwiperは元の実装上-[UIPanGestureRecognizer velocityInView:]
を使っていたのでそのまま-velocityInView:
を使い解決 (Mergeされました)しましたが、UIGestureRecognizer (またはサブクラス)
でもUIResponderで行なっている様な判定で対応できると思います。