Как iOS определяет, какое представление (UIView) получило касание?

«Как iOS определяет, какое представление (UIView) получило касание?» — вопрос из категории UIKit, который задают на 22% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Для определения целевого UIView при касании iOS использует процесс hit-testing, который координируется методом hitTest(_:with:) у UIWindow и UIView.

Алгоритм hit-testing (поиска цели):

  1. Система начинает с корневого окна (UIWindow) и передает ему точку касания.
  2. Для каждой view рекурсивно вызывается hitTest(_:with:), который работает так:
    • A. Проверка возможности быть целью: Вызывает point(inside:with:), чтобы проверить, находится ли точка в bounds view. Также автоматически проверяет:
      • isUserInteractionEnabled == true
      • isHidden == false
      • alpha > 0.01
      • Если условие не выполняется — возвращает nil.
    • B. Рекурсивный опрос subviews: Если точка внутри, метод проходит по массиву subviews в обратном порядке (от самого верхнего к нижнему). Для каждого subview:
      • Конвертирует точку в систему координат subview.
      • Рекурсивно вызывает у subview hitTest(_:with:).
      • Если subview возвращает не-nil (нашел свою цель), этот результат и возвращается.
    • C. Возврат себя: Если точка внутри view, но ни один из subviews не вернул цель, то эта view и становится целевой. Метод возвращает self.

Упрощенная реализация (иллюстрирующая логику):

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    // 1. Быстрая проверка возможности быть целью
    guard self.point(inside: point, with: event),
          isUserInteractionEnabled,
          !isHidden,
          alpha > 0.01 else {
        return nil
    }

    // 2. Рекурсивный опрос subviews (сверху вниз)
    for subview in subviews.reversed() {
        let convertedPoint = subview.convert(point, from: self)
        if let candidate = subview.hitTest(convertedPoint, with: event) {
            return candidate // Нашли цель в иерархии subview
        }
    }

    // 3. Эта view — конечная цель
    return self
}

Практическое применение:

  • Увеличение области касания: Переопределить point(inside:with:), чтобы возвращать true для области больше bounds.
  • Игнорирование касаний: Вернуть nil в hitTest(_:with:), чтобы view "пропускала" события сквозь себя.
  • Перенаправление событий: Вернуть другую view в качестве результата hitTest(_:with:).