В чем разница между методами point(inside:with:) и hitTest(_:with:) в UIKit?

Ответ

Эти методы работают в тандеме для определения получателя touch-событий в иерархии UIView.

point(inside:with:)

  • Назначение: Проверяет, находится ли заданная точка в пределах границ (bounds) текущего view.
  • Возвращает: Bool (true, если точка внутри).
  • Логика: Простая геометрическая проверка. Не учитывает иерархию subviews.
  • Типичное использование: Переопределяется для расширения или изменения области отклика view без изменения его видимых границ.
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        // Увеличиваем область отклика на 10 пунктов во все стороны
        let expandedBounds = bounds.insetBy(dx: -10, dy: -10)
        return expandedBounds.contains(point)
    }

hitTest(_:with:)

  • Назначение: Определяет самое глубокое (frontmost) view в иерархии, которое должно получить touch-событие для заданной точки.
  • Возвращает: UIView? (найденное view или nil).
  • Алгоритм работы (рекурсивный):
    1. Проверяет, может ли view вообще получать события: isUserInteractionEnabled, !isHidden, alpha > 0.01.
    2. Вызывает point(inside:with:) для себя. Если точка вне границ — возвращает nil.
    3. Если точка внутри, проходит по subviews в обратном порядке (от самого верхнего к нижнему), вызывая для каждого hitTest.
    4. Если один из subviews вернул не-nil view, возвращает его. Если нет — возвращает себя.
  • Типичное использование: Переопределяется для перенаправления событий или создания нестандартного поведения (например, игнорирование touches в определенной области).
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let hitView = super.hitTest(point, with: event)
        // Если событие должно попасть в прозрачную кнопку-оверлей, возвращаем её
        return (hitView == self) ? overlayButton : hitView
    }

Взаимосвязь: hitTest использует point(inside:with:) как один из шагов в своем алгоритме. Переопределение point(inside:with:) напрямую влияет на результат работы hitTest для этого view.

Ответ 18+ 🔞

А, ну это ж классика, блядь! Эти два метода — как Герасим и его Муму, один другого кормит, а потом оба в озеро летят, если не разобраться. Слушай, сейчас разжую, как сука, чтобы понятно было.

Вот представь себе, блядь, у тебя на экране эта пирамида из вьюх, как матрёшки, одна в другой. Палец тыкнул — кому событие достанется? А вот эти два мудака и решают, ёпта!

point(inside:with:) — это как охранник у двери, тупой и прямолинейный.

Его задача — чисто геометрическая, блядь. Стоит на входе в конкретный клуб под названием «Вьюха» и орет: «Эй, ты, точка! Ты внутри моих границ? Да или нет?». Никаких subviews, никакой иерархии — просто прямоугольник. bounds.contains(point), и всё, пиздец.

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    // Делаем зону отклика побольше, чтоб даже криворукие попали
    let expandedBounds = bounds.insetBy(dx: -10, dy: -10)
    return expandedBounds.contains(point) // true/false, и нехуй тут
}

Переопределяешь его, когда тебе надо, чтоб кнопка откликалась, даже если тыкнул рядом, или наоборот — чтоб игнорировал дырку посередине. Охранник просто расширил или сузил проход, блядь.

hitTest(_:with:) — это уже главный администратор всего этого борделя.

Он умный, рекурсивный и знает всю иерархию наизусть. Его работа — найти самую верхнюю, самую глубоко сидящую вьюху, которой и вручить этот самый touch. Алгоритм у него, блядь, строгий:

  1. Фильтр дебилов: Смотрит на вьюху — включен ли юзер-интеракшен, не спрятана ли, альфа не нулевая? Если что-то не так — сразу nil, иди нахуй, не кондиция.
  2. Вызов охранника: Кричит тому самому point(inside:with:): «Эй, охранник! Эта точка у нас в клубе?». Если нет — опять nil, мимо проходили.
  3. Рекурсивный обход: Если точка внутри, он начинает опрашивать всех своих subviews, но с конца, с самого верхнего (потому что тот поверх всех лежит, его и трогают первым). Каждому задаёт тот же вопрос: «А у тебя кто внутри сидит?».
  4. Вердикт: Если какой-то subview нашёлся и вернул себя — его и отдаём. Если все subviews пустые — ну значит, событие этому самому родителю и достаётся, он и есть самый глубокий.

Переопределяешь его, когда надо событие перенаправить, ёбана. Например, твоя вьюха прозрачная, но через неё должна тыкаться кнопка-оверлей.

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let hitView = super.hitTest(point, with: event) // спросим у предка
    // Если событие пришло ко мне самому, а не к детям — отдадим его оверлейной кнопке
    return (hitView == self) ? overlayButton : hitView
}

А связь-то, блядь, в чём? Да в том, что hitTest использует point(inside:with:) на каждом шагу! Это как система, ёпта. Охранник говорит «не пущаю» — администратор даже не станет внутрь смотреть. Переопределил point — автоматически изменил логику для hitTest. Один другого кормит, пока не прикажут собаку утопить, вот и вся философия, в рот меня чих-пых!