重要ポイント
UIGestureRecognizerはUIKitの抽象基底クラスで、ジェスチャー認識プロセスを個別のオブジェクトに分割します。各サブクラスは1つのジェスチャータイプを処理します。UITapGestureRecognizerは特定のタッチ数のタップを処理し、UISwipeGestureRecognizerは指定された方向のスワイプを処理します。開発者はaddGestureRecognizer(:)メソッドを介してUIViewにレコグナイザーを追加し、UIKitが自動的にタッチを追跡し、状態を更新し、認識時にアクションを呼び出します。
UIGestureRecognizer(iOS 3.2、2010年)以前は、開発者はUIResponderメソッド(touchesBegan、touchesMoved、touchesEnded)をオーバーライドし、タッチの軌跡を手動で分析していました。これによりコードの重複と同時タッチ処理時のエラーが発生していました。AppleはこのロジックをUIGestureRecognizerにカプセル化し、マルチタッチ、ジェスチャーのキャンセル、1つのビューでの複数レコグナイザーの同時動作をサポートしました。
WWDCセッションによると、Gesture RecognizerはiPadで最大11の同時タッチ、iPhoneで5つの同時タッチを処理します。IT Sectrでは、すべてのUIKitプロジェクトでユーザー入力処理の標準方法としてUIGestureRecognizerを使用しています。これにより手動のtouchesBegan追跡に関連するバグが排除されました。
各UIGestureRecognizerは7つの可能な状態を経由します。これらはUIGestureRecognizer.State列挙型で定義されています。これらの状態は、タッチ検出から完了またはキャンセルまでの認識ライフサイクルを反映します。状態を理解することは、カスタムレコグナイザーの実装と競合のデバッグに不可欠です。
| 状態 | 意味 | 発生タイミング |
|---|---|---|
| .possible | 初期状態、ジェスチャーはまだ認識されていない | ビューに追加された直後 |
| .began | ジェスチャーが認識され実行を開始した | pan/longPressで最初の指の動き時 |
| .changed | ジェスチャーのパラメータが変更(座標、角度) | 指が動くたび |
| .ended | ユーザーが指を離し、ジェスチャー完了 | touchesEnded時 |
| .cancelled | システムによりジェスチャーが中断(着信、画面回転など) | touchesCancelled時 |
| .failed | 条件に基づきジェスチャーが認識されなかった | 認識なしのtouchesCancelled時 |
| .recognized | .endedの同義語、ジェスチャーが正常に認識された | .endedと同じ |
離散ジェスチャー(タップ、スワイプ)は.possibleから直接.endedまたは.failedに移行します。連続ジェスチャー(パン、ピンチ、ローテーション、長押し)は.possible → .began → .changed(繰り返し)→ .endedを経由します。アクションメソッドでgestureRecognizer.stateを確認することで、ジェスチャーの開始、変更、終了を区別できます。
UIKitは7つの組み込みUIGestureRecognizerサブクラスを提供し、ほとんどのインタラクションシナリオをカバーしています。各サブクラスには特定の設定があります:タップのnumberOfTapsRequired、スワイプのdirection、長押しのminimumPressDurationなどです。
カスタムジェスチャー(例えばジグザグ描画)の場合は、touchesBegan、touchesMoved、touchesEndedをオーバーライドし状態を更新するUIGestureRecognizerのサブクラスを作成します。Appleは可能な限り組み込みクラスを使用することを推奨しています。これらは最適化されており、相互に正しく連携します。
1つのUIViewで複数のUIGestureRecognizerを使用する場合(タップとダブルタップなど)、認識競合が発生します。ダブルタップ時にシングルタップが先に発火します。Appleはrequire(toFail:)メソッドを提供し、一方のジェスチャーの認識を他方が失敗するまで遅延させます。
このメカニズムは次のように機能します。tapRecognizer.require(toFail: doubleTapRecognizer)を呼び出すことで、doubleTapRecognizerが.failedで終了した後にのみtapRecognizerが.recognized状態に移行するよう指定します。これによりシングルタップの実行前に約0.3秒の遅延が追加されます。ユーザーが2回タップすると最初のタップは無視されます。代替アプローチとして、UIGestureRecognizerDelegateのgestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)メソッドを使用すると、同時認識が可能になります(例:マップのpan + pinch)。
WWDC 2020によると、UIKitアプリケーションのバグの約15%がジェスチャー競合の不適切な設定に関連しています。IT Sectrではアプローチを標準化しました。各画面にrequire(toFail:)の優先順位を持つジェスチャースキームを設定し、ダブルタップ発火のバグを完全に排除しました。
UIImageViewにシングルタップハンドラーを追加します。タップすると画像の不透明度が変化します。ビューへのジェスチャーバインディングを示す簡単な例です。
import UIKit
class ImageViewController: UIViewController {
@IBOutlet private var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(
target: self,
action: #selector(handleTap(_:))
)
tap.numberOfTapsRequired = 1
imageView.addGestureRecognizer(tap)
imageView.isUserInteractionEnabled = true
}
@objc private func handleTap(_: UITapGestureRecognizer) {
UIView.animate(withDuration: 0.2) {
self.imageView.alpha = self.imageView.alpha == 1.0 ? 0.5 : 1.0
}
}
}
重要なポイント:UIImageViewのisUserInteractionEnabledはデフォルトでfalseです。このフラグがないと、Gesture Recognizerはタッチを受け取りません。UIViewとUIButtonではこのフラグはデフォルトで有効です。アクションメソッドはUITapGestureRecognizerパラメーターを受け取り、location(in:)でタップ座標を取得できます。
左スワイプで前の画面に戻る機能を実装します。方向設定とコントローラーのルートビューへのジェスチャーバインディングを示します。
import UIKit
class DetailViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let swipeLeft = UISwipeGestureRecognizer(
target: self,
action: #selector(handleSwipe(_:))
)
swipeLeft.direction = .left
view.addGestureRecognizer(swipeLeft)
}
@objc private func handleSwipe(_: UISwipeGestureRecognizer) {
navigationController?.popViewController(animated: true)
}
}
UISwipeGestureRecognizerは離散ジェスチャーです。認識後すぐに.recognizedに移行し、中間の.changed状態はありません。したがって、アクションで状態を確認する必要はありません。ジェスチャーが認識されるか(アクションが発火)、認識されないかのいずれかです。directionプロパティは.left、.right、.up、.downの4つの値のいずれかを受け入れます。複数の方向をサポートするには、方向ごとに個別のレコグナイザーを作成します。
競合なく1つのビューでシングルタップとダブルタップを設定する方法を示します。ダブルタップレコグナイザーに優先度があり、シングルタップはダブルタップが認識されない場合にのみ発火します。
import UIKit
class TapViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let singleTap = UITapGestureRecognizer(
target: self,
action: #selector(handleSingleTap)
)
singleTap.numberOfTapsRequired = 1
let doubleTap = UITapGestureRecognizer(
target: self,
action: #selector(handleDoubleTap)
)
doubleTap.numberOfTapsRequired = 2
singleTap.require(toFail: doubleTap)
view.addGestureRecognizer(singleTap)
view.addGestureRecognizer(doubleTap)
}
@objc private func handleSingleTap() {
print("Single tap — after 0.3s delay")
}
@objc private func handleDoubleTap() {
print("Double tap — instant")
}
}
require(toFail:)がない場合、ダブルタップ時にhandleSingleTapが最初に発火し、次にhandleDoubleTapが発火します。これによりUXが損なわれます。require(toFail:)を使用すると、シングルタップは約0.3秒待機して2回目のタップが来ないことを確認します。IT Sectrでは、このパターンが画像エディターやギャラリーで使用されており、ダブルタップでズーム、シングルタップで要素選択を行います。
よくある質問
はい、UIViewは複数のUIGestureRecognizerインスタンスを同時にサポートします。競合を解決するには、認識順序を設定するrequire(toFail:)メソッドを使用します。ジェスチャーの並行操作(例:マップのpan + pinch)には、trueを返すgestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)デリゲートメソッドを実装します。
UIGestureRecognizerはタッチパターンを自動認識し状態を管理する高レベル抽象化です。touchesBeganはUIResponderの低レベルメソッドで、手動による座標追跡、タイミング、タッチキャンセルが必要です。Gesture Recognizerはシンプルで信頼性が高く、標準ジェスチャーに適しています。touchesBeganはカスタムグラフィックスにのみ使用すべきです。
ネイティブSwiftUIではモディファイア(onTapGesture、onLongPressGesture、DragGesture、MagnificationGesture、RotationGesture)を使用します。これらはSwiftUI階層に統合されたUIGestureRecognizerの宣言的アナログです。必要に応じてUIViewRepresentableを介してUIKitレコグナイザーをラップできますが、AppleはネイティブSwiftUIジェスチャーの使用を推奨しています。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。