Главное
UIGestureRecognizer — абстрактный базовый класс в UIKit, декомпозирующий процесс распознавания жестов в отдельные объекты. Каждый подкласс отвечает за один тип жеста: UITapGestureRecognizer — за тап определённого количества пальцев, UISwipeGestureRecognizer — за свайп в заданном направлении. Разработчик добавляет распознаватель на UIView через метод addGestureRecognizer(:), и UIKit автоматически отслеживает касания, обновляет состояние и вызывает action при распознавании.
До появления UIGestureRecognizer (iOS 3.2, 2010) разработчики переопределяли методы UIResponder — touchesBegan, touchesMoved, touchesEnded — и вручную анализировали траекторию касаний. Это приводило к дублированию кода и ошибкам при обработке одновременных касаний. Apple инкапсулировала эту логику в UIGestureRecognizer, добавив поддержку мультитач, отмены жестов и одновременной работы нескольких распознавателей на одной вью.
Согласно WWDC сессиям, Gesture Recognizer обрабатывает до 11 одновременных касаний на iPad и 5 на iPhone. В IT Sectr мы используем UIGestureRecognizer как стандартный способ обработки пользовательского ввода во всех UIKit-проектах — это исключило баги, связанные с ручным отслеживанием touchesBegan.
Каждый UIGestureRecognizer проходит через 7 возможных состояний, определённых в enum UIGestureRecognizer.State. Эти состояния отражают жизненный цикл распознавания: от определения касания до завершения или отмены жеста. Понимание состояний критично для реализации кастомных распознавателей и отладки конфликтов.
| Состояние | Значение | Когда возникает |
|---|---|---|
| .possible | Начальное состояние, жест ещё не распознан | Сразу после добавления на вью |
| .began | Жест распознан и начал выполняться | При первом движении пальца для pan/longPress |
| .changed | Параметры жеста изменились (координаты, угол) | При каждом движении пальца |
| .ended | Пользователь убрал палец, жест завершён | При touchesEnded |
| .cancelled | Жест прерван системой (входящий звонок, смена ориентации) | При touchesCancelled |
| .failed | Жест не распознан по условиям | При touchesCancelled без распознавания |
| .recognized | Синоним .ended; жест успешно распознан | То же, что .ended |
Дискретные жесты (tap, swipe) переходят из .possible сразу в .ended или .failed. Непрерывные жесты (pan, pinch, rotation, longPress) проходят .possible → .began → .changed (многократно) → .ended. В action-методе проверяйте gestureRecognizer.state — это позволяет различать начало, изменение и конец жеста.
UIKit предоставляет 7 встроенных подклассов UIGestureRecognizer, покрывающих большинство сценариев взаимодействия. Каждый подкласс имеет специфические настройки: numberOfTapsRequired для тапа, direction для свайпа, minimumPressDuration для долгого нажатия.
Для кастомных жестов (например, рисование зигзага) создаётся подкласс UIGestureRecognizer с переопределением методов touchesBegan, touchesMoved, touchesEnded и обновлением state. Apple рекомендует использовать встроенные классы где возможно — они оптимизированы и корректно взаимодействуют друг с другом.
Когда на одной UIView используется несколько UIGestureRecognizer (например, tap и double-tap), возникает конфликт распознавания: при двойном тапе сначала срабатывает одиночный tap. Для решения Apple предоставляет метод require(toFail:), который задерживает распознавание одного жеста, пока другой не завершится неудачей.
Механизм работает так: вызывая tapRecognizer.require(toFail: doubleTapRecognizer), вы указываете, что tapRecogniser перейдёт в состояние .recognized только после того, как doubleTapRecogniser завершится в .failed. Это добавляет задержку ~0.3 секунды перед выполнением одиночного тапа — пользователь жмёт дважды, а первый тап игнорируется. Альтернативный метод — делегат UIGestureRecognizerDelegate с методом gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:), который разрешает одновременное распознавание (например, pan + pinch для карты).
По данным WWDC 2020, около 15% багов в UIKit-приложениях связаны с неправильной настройкой конфликтов жестов. В 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
}
}
}
Ключевой момент: isUserInteractionEnabled на UIImageView по умолчанию равен false — без этого флага Gesture Recognizer не будет получать касания. Для UIView и UIButton флаг включён по умолчанию. action-метод принимает параметр UITapGestureRecognizer, через который можно получить location(in:) для определения координат тапа.
Реализует свайп влево для возврата на предыдущий экран. Демонстрирует настройку направления и привязку жеста к root-вью контроллера.
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 — дискретный жест: он переходит в .recognised сразу после распознавания, без промежуточных .changed. Поэтому в action не нужно проверять state — жест либо распознан (вызов action), либо нет. Свойство direction принимает одно из четырёх значений: .left, .right, .up, .down. Для поддержки нескольких направлений создайте отдельные распознаватели на каждое.
Показывает, как настроить одиночный и двойной тап на одной вью без конфликта. Распознаватель двойного тапа имеет приоритет — одиночный тап срабатывает только если двойной не распознан.
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 секунды, чтобы убедиться, что второй тап не последует. В IT Sectr этот паттерн используется в редакторах изображений и галереях, где двойной тап зуммирует, а одиночный — выделяет элемент.
Часто задаваемые вопросы
Да, UIView поддерживает множество UIGestureRecognizer одновременно. Для разрешения конфликтов используйте метод require(toFail:), который задаёт порядок распознавания. Для параллельной работы жестов (например, pan + pinch на карте) реализуйте делегат gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:), возвращающий true.
UIGestureRecognizer — высокоуровневая абстракция, распознающая паттерны касаний автоматически и управляющая состояниями. touchesBegan — низкоуровневый метод UIResponder, требующий ручного отслеживания координат, тайминга и отмены касаний. Gesture Recognizer проще, надёжнее и предпочтительнее для стандартных жестов; touchesBegan оправдан только для кастомной графики.
В нативном SwiftUI используются модификаторы: onTapGesture, onLongPressGesture, DragGesture, MagnificationGesture, RotationGesture. Это декларативные аналоги UIGestureRecognizer, интегрированные в SwiftUI-иерархию. При необходимости можно обернуть UIKit-распознаватель через UIViewRepresentable, но Apple рекомендует использовать нативные SwiftUI-жесты.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также