UIGestureRecognizerの検知速度に悩まされたお話

はじめに

ご挨拶

はじめまして、m_yamadaです。
ときどきQ○itaに投稿してて、せっかくなら社員ブログに投稿してーとお声がかかっての初投稿です。

投稿するお題は自由と言い渡されたので、
今回はiOSですが他のカテゴリでも投稿するかもしれないです。

自己紹介は以上になります。
以降、記事タイトル通りの本題です。

何をやりたかったか

シングルタップのイベントとダブルタップのイベントの検知共存。
ついでにロングタップイベントも共存。

どんな実装?

グーグル先生に問い合わせると上位に出てくるお馴染みの実装。
具体的に、

シングルタップのイベントとダブルタップのイベントの検知

の競合対策実装が以下(Swift3.1)。

required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// ダブルタップ
let doubleTapGesuture = UITapGestureRecognizer(
target: self, action: #selector(self.doubleTap(_:)))
doubleTapGesuture.numberOfTapsRequired = 2
self.addGestureRecognizer(doubleTapGesuture)

// シングルタップ
let singleTapGesture = UITapGestureRecognizer(
target: self, action: #selector(self.singleTap(_:)))
singleTapGesture.numberOfTapsRequired = 1
// ダブルタップの場合、シングルタップは失敗させる
singleTapGesture.require(toFail: doubleTapGesuture)
self.addGestureRecognizer(singleTapGesture)
}

UIGestureRecognizer利用の問題点

タップを検知して、イベントが走るまでに時間がかかること。

ボタンや、画面上の何らかのオブジェクト上で発生する
タップイベントを拾う分にはぜんっぜん気にならない。気になる機会がなかった。

ただ、タップイベントを拾った際の処理で表示レイヤーへ描画をごりごりするという
お絵かき機能染みたことをやろうとすると、すごく気になる遅さだった。

具体的にどのくらい時間がかかる?

UITouchクラスのtouchesEndedメソッドが走った後、
上記のsingleTapGestureに仕掛けたメソッドが走るまでの時間を測ってみる。
時間計測方法は以下の通り。

override open func touchesEnded(
_ touches: Set, with event: UIEvent?) {
self.touchedTime = NSDate()
}

open func singleTap(_ gestureRecognizer: UITapGestureRecognizer) {
let elapsed = -self.touchedTime!.timeIntervalSinceNow
print("touchesEnded動作後からイベント発動までの時間:\(elapsed)")
}

実行結果が以下の通り。

touchesEnded動作後からイベント発動までの時間:0.356702983379364
touchesEnded動作後からイベント発動までの時間:0.368847012519836
touchesEnded動作後からイベント発動までの時間:0.356013000011444

これだけ時間かかると、タップした後に人間の目で十分視認できるレベルで
一瞬何もイベントが発生せず、「あれ?」ってなる。

画面をタップされたあと、
UITouchクラスでは画面をどのように動いたかをそのまま返し、
UIGestureRecognizerクラスではタップ・スワイプ・ピンチイン/アウトなどなど
最終的にどのジェスチャーに該当する操作がされたかの分類を行うようなので無理もない。

タップイベント検知速度の改善策

UIGestureRecognizer利用を諦める

代わりに、UITouchクラスのtouches~メソッドで対応。
今回使用するのは、touchesBeganとtouchesEnded。

touchesEnded

override open func touchesEnded(
_ touches: Set, with event: UIEvent?) {
let touch = touches.first!

if touch.tapCount == 1 && self.drawPointMode {
// シングルタップ用の処理は若干遅延して発動させる
self.singleTapEventTimer = Timer.scheduledTimer(
timeInterval: 0.2,
target: self,
selector: #selector(self.singleTap(_:)),
userInfo: touch,
repeats: false
)
} else if touch.tapCount == 2 {
// tapCountが2の場合はダブルタップ用の処理を発動
self.doubleTap(touch)
}
}

open func singleTap(_ timer: Timer!) {
let touch = timer.userInfo as! UITouch
// シングルタップ用の処理
}

ポイントは、以下。

// シングルタップ用の処理は若干遅延して発動させる
self.singleTapEventTimer = Timer.scheduledTimer(
timeInterval: 0.2,
target: self,
selector: #selector(self.singleTap(_:)),
userInfo: touch,
repeats: false
)

シングルタップ用の処理をタップを検知してすぐに発動、ではなく
次のタップイベントが発生しないか確認してから発動するように仕掛ける。

※0.2秒は感覚で指定してあり、実際に何秒までダブルタップ
(UITouchクラスのtapCountプロパティで2を拾えるか)として検知できるのかは未検証

touchesBegan

override open func touchesBegan(
_ touches: Set, with event: UIEvent?) {
self.touchedTime = NSDate()
// 連続して2回のタップイベントを検知時、シングルタップ用の処理はキャンセル
let touch = touches.first!
if touch.tapCount == 2 {
self.singleTapEventTimer?.invalidate()
}
}

1回目のタップ時にtouchesEndedメソッドで仕掛けた処理が発動する前に
2回目のタップ時のtouchesBeganメソッドが走った場合はシングルタップ用の処理をキャンセルし、
この後のtouchesEndedメソッドにてダブルタップ用の処理を発動させる。

これで視認できる不自然な遅延も発生しなくなり、ダブルタップイベント検知も共存できた。

補足

冒頭の

ついでにロングタップイベントも共存

については、ダブルタップイベントの拾い方の条件で解決している。

if touch.tapCount == 2 {

ロングタップの場合、tapCountが0になる。
ロングタップ用の処理を別途touchesMovedメソッド辺りに仕掛ける場合に
他の処理と競合させたくない場合、tapCount == 0でイベントを拾えばOK。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>