Когда и как срабатывает Hit-Testing в UIKit?

Ответ

Hit-Testing — это процесс определения, какое представление (UIView) должно получить событие касания. Он запускается при каждом новом касании (touch event) системой UIApplication.

Алгоритм (метод hitTest(_:with:)): Процесс рекурсивный, начинается с корневого окна (UIWindow) и идет от верхних вью к нижним.

  1. Проверяются условия для текущей вью:
    • isUserInteractionEnabled == true
    • isHidden == false
    • alpha > 0.01
    • Точка касания находится внутри границ вью (bounds).
  2. Если условия не выполнены – вью и всё её поддерево игнорируются.
  3. Если условия выполнены, алгоритм проходит по массиву subviews в обратном порядке (от последней добавленной, которая визуально выше, к первой).
  4. Для каждой сабвью рекурсивно вызывается hitTest.
  5. Если сабвью возвращает не-nil (нашла подходящую вью), этот результат возвращается.
  6. Если ни одна сабвью не вернула результат, возвращается сама текущая вью.

Пример переопределения для расширения области касания:

class CustomButton: UIButton {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // 1. Проверяем стандартные условия
        guard self.isUserInteractionEnabled,
              !self.isHidden,
              self.alpha > 0.01 else {
            return nil
        }
        // 2. Расширяем область проверки на 20 пунктов во все стороны
        let expandedBounds = bounds.insetBy(dx: -20, dy: -20)

        // 3. Если точка в расширенной области, возвращаем self (эту кнопку)
        if expandedBounds.contains(point) {
            return self
        }
        // 4. Иначе передаем обработку стандартному алгоритму
        return super.hitTest(point, with: event)
    }
}

Итог: Hit-Testing гарантирует, что событие касания получает самый глубокий (верхний в иерархии) UIView, готовый его обработать.

Ответ 18+ 🔞

Слушай, а вот эта ваша хит-тестинговая магия — это ж просто пиздец какой-то цирк с конями, ей-богу. Сидит себе система, как хитрая жопа, и думает: «Ну-ка, чувак, куда ты там ткнул своим жирным пальцем?» И начинает этот ебёный детектив.

Представь: каждый раз, как ты тыкаешь в экран, UIApplication такой: «Опа, новое касание, блядь! Ну-ка, UIWindow, давай, работай, сука!» И пошла писать губерния.

Алгоритм, он же метод hitTest(_:with:) — это рекурсивный пиздец. Начинается всё с корневого окна и идёт сверху вниз, то есть от тех вьюх, которые визуально наверху, к тем, что под ними.

  1. Сначала вьюху на месте преступления допрашивают по трём пунктам:
    • isUserInteractionEnabled == true — чтоб с ней вообще можно было взаимодействовать, а не как с пнём.
    • isHidden == false — чтоб она не была спрятана, как маньяк в кустах.
    • alpha > 0.01 — чтоб её хоть чуть видно было, а не как призрак.
    • И главное — точка касания должна быть внутри её границ (bounds). Не попал — иди нахуй.
  2. Если хоть одно условие провалено — вся эта вьюха и её дети-сабвьюхи отправляются в игнор. Пиздуй отсюда.
  3. Если же всё чики-пуки, начинается самое интересное. Алгоритм лезет в массив subviews и проходит по нему в обратном порядке. То есть смотрит сначала на ту, которую добавили последней и которая, блядь, сейчас сверху всех лежит.
  4. Для каждой такой сабвьюхи он рекурсивно вызывает этот же самый hitTest. И так, сука, углубляется, пока не упрётся.
  5. Если какая-то сабвьюха нашла себя и возвращает не-nil — всё, квест завершён, результат всплывает наверх.
  6. Если все дети молчат как рыбы — ну что ж, тогда возвращается сама текущая вьюха. «Значит, ткнули именно в меня, блядь».

Вот тебе пример, как можно наёбнуть систему и расширить зону клика, если кнопка мелкая, как хуй с горы:

class CustomButton: UIButton {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // 1. Сначала стандартные проверки, без них нихуя не получится
        guard self.isUserInteractionEnabled,
              !self.isHidden,
              self.alpha > 0.01 else {
            return nil
        }
        // 2. Расширяем область проверки на 20 пунктов во все стороны, чтоб легче было попасть
        let expandedBounds = bounds.insetBy(dx: -20, dy: -20)

        // 3. Если ткнули в эту расширенную зону — о, сука, попал! Возвращаем саму кнопку.
        if expandedBounds.contains(point) {
            return self
        }
        // 4. Если не попали — ну что ж, пусть родительский алгоритм разбирается
        return super.hitTest(point, with: event)
    }
}

Итог, блядь: Весь этот хит-тестинг существует для одного — чтобы событие касания получил самый глубокий (и визуально верхний) UIView, который готов его принять. Всё честно, как в хорошем борделе: кто первый встал, того и тапки.