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

Ответ

Для определения целевого 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:).

Ответ 18+ 🔞

Ну ты смотри, какие страсти в айфоне творятся, когда ты пальцем по экрану шаркаешь! А там, блядь, целая операция под кодовым названием hit-testing идёт, сука! Это как в тире: система ищет, в какую вьюху ты пальцем ткнул, чтобы ей событие впендюрить.

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

  1. Начинается всё с главного окна (UIWindow), корня всего сущего. Ему и передают координаты твоего тычка.
  2. А дальше пошла рекурсия, мать её! Для каждой вьюхи дергается метод hitTest(_:with:), который работает по чёткому плану:
    • А. Проверка на профпригодность: Сначала дергается point(inside:with:), чтобы понять, вообще в границы этой вьюхи точка попадает? Заодно автоматом глядит:
      • isUserInteractionEnabled == true (чтоб не выключенная)
      • isHidden == false (чтоб не спрятанная)
      • alpha > 0.01 (чтоб хоть чуть видимая)
      • Если хоть одно условие не прошёл — всё, пизда, возвращает nil. Не кандидат.
    • Б. Опрос подчинённых: Если точка внутри, начинается самое интересное. Метод лезет в массив subviews и проходит по нему ЗАДОМ НАПЕРЁД (от верхних слоёв к нижним). Для каждой дочерней вьюхи:
      • Пересчитывает точку в её координаты.
      • И, охуеть, рекурсивно вызывает у этой детки её собственный hitTest(_:with:).
      • Если какая-то subview нашла в себе цель и вернула не nil — всё, поиск окончен, возвращается эта цель. Дальше не ищем, блядь!
    • В. Самозванство: Если точка внутри этой вьюхи, но все её subviews развели руками (вернули nil), значит цель — она сама, красавица! Метод возвращает 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. Опрашиваем детей, начиная с самых верхних (тех, что позже добавили)
    for subview in subviews.reversed() {
        let convertedPoint = subview.convert(point, from: self)
        if let candidate = subview.hitTest(convertedPoint, with: event) {
            return candidate // Всё, нашли виноватого, дальше не ищем!
        }
    }

    // 3. Дети не при делах, значит это я и есть тот самый красавчик
    return self
}

А где это, блядь, применить-то можно?

  • Сделать кнопку больше, чем она есть: Переопределить point(inside:with:), чтобы она возвращала true для области шире её видимых границ. Хитрая жопа!
  • Сделать вьюху-невидимку: Вернуть nil в своём hitTest(_:with:), и все события будут пролетать сквозь неё, как сквозь дуршлаг. Идеально для декоративных хуёвин.
  • Подставить другого: Вернуть в hitTest(_:with:) не себя, а другую вьюху. Вот так просто, блядь, перенаправить событие! Главное — не запутаться, а то получится пиздопроебибна.