Как работает hit-test в UIKit: рекурсивно или итеративно?

Ответ

В UIKit hit-test выполняется рекурсивно. Алгоритм начинается с корневого view (обычно UIWindow) и движется вниз по иерархии.

Процесс:

  1. Проверяется, находится ли точка касания в пределах bounds текущего view.
  2. Если да, метод рекурсивно вызывается для его subviews в обратном порядке (последний добавленный проверяется первым).
  3. Если точка не попадает ни в один subview, возвращается сам view.
  4. Если view скрыто (isHidden), отключено для взаимодействия (isUserInteractionEnabled) или имеет почти нулевую прозрачность (alpha <= 0.01), оно игнорируется.

Пример реализации hitTest(_:with:):

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    // 1. Проверка возможности взаимодействия
    guard isUserInteractionEnabled, !isHidden, alpha > 0.01 else {
        return nil
    }
    // 2. Проверка попадания точки в bounds
    guard self.point(inside: point, with: event) else {
        return nil
    }
    // 3. Рекурсивный обход subviews (в обратном порядке)
    for subview in subviews.reversed() {
        let convertedPoint = subview.convert(point, from: self)
        if let hitView = subview.hitTest(convertedPoint, with: event) {
            return hitView
        }
    }
    // 4. Точка внутри этого view, но не внутри его subviews
    return self
}

Почему рекурсивно? Это естественно отражает древовидную структуру view-иерархии и позволяет каждому view контролировать свою логику попадания (например, через переопределение этого метода).

Ответ 18+ 🔞

А, ну это же классика, блядь! UIKit и его хит-тест, ёпта. Сидит себе система, как хитрая жопа, и решает, куда ты пальцем ткнул. А под капотом-то — рекурсия, мать её!

Представь: у тебя окно, в нём вьюшка, в ней ещё три, а в одной из них кнопка спрятана. Ты тыкаешь в экран, а система начинает, как маньяк, сверху вниз шариться: «Тут? Не-а. А тут? Во, кажется, попал! А у тебя внутри кто живёт?».

Как оно, сука, работает, по шагам:

  1. Попадание в границы? Сначала смотрит: а в меня ли вообще тыкнули? Если точка за пределами bounds — сразу отвал, возвращает nil. Нечего тут шастать.
  2. А живой ли я? Проверяет, не скрыт ли вью (isHidden), не выключен ли (isUserInteractionEnabled) и не стал ли он призраком (alpha <= 0.01). Если что-то не так — всё, игнорируется, как пустое место. Прозрачнее 1% — и ты уже призрак, блядь!
  3. Рекурсия — наше всё! Если сам-то вью живой и точка в нём, он начинает допрос своих детей — subviews. Идёт он по ним задом наперёд! Кто последним добавлен (тот, кто обычно сверху визуально), тот первым и проверяется. Логично же, сука: что сверху, то и ближе к пальцу.
  4. Финал. Если ни один ребёнок не признался, что в него попали, значит, тыкнули прямо в этого родителя. Он и возвращается со словами: «Да, это я, чё надо?».

Вот смотри, как это в коде выглядит, реальная логика:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    // 1. Я вообще живой и кликабельный?
    guard isUserInteractionEnabled, !isHidden, alpha > 0.01 else {
        return nil // Нет, иди нахуй.
    }
    // 2. В меня ли тыкнули?
    guard self.point(inside: point, with: event) else {
        return nil // Мимо, промазал.
    }
    // 3. Опрашиваю детей, начиная с того, кто сверху (reversed)
    for subview in subviews.reversed() {
        // Перевожу точку в его систему координат
        let convertedPoint = subview.convert(point, from: self)
        // Рекурсивно спрашиваю: "Эй, пацан, это ты?"
        if let hitView = subview.hitTest(convertedPoint, with: event) {
            return hitView // Он говорит "да" — значит, он и есть цель.
        }
    }
    // 4. Дети молчат — значит, попали в меня самого.
    return self
}

А нахуя рекурсия-то? Да потому что иерархия вьюшек — это же дерево, ёпта! Каждая ветка (вью) должна сама решать, как на неё кликать. Можешь переопределить этот метод и сделать, например, кликабельную область больше, чем bounds. Или наоборот, дыру в середине создать. Вся мощь — в этом рекурсивном спуске, где каждый может сказать своё «муму», блядь!