Ответ
Для определения целевого UIView при касании iOS использует процесс hit-testing, который координируется методом hitTest(_:with:) у UIWindow и UIView.
Алгоритм hit-testing (поиска цели):
- Система начинает с корневого окна (
UIWindow) и передает ему точку касания. - Для каждой view рекурсивно вызывается
hitTest(_:with:), который работает так:- A. Проверка возможности быть целью: Вызывает
point(inside:with:), чтобы проверить, находится ли точка вboundsview. Также автоматически проверяет:isUserInteractionEnabled == trueisHidden == falsealpha > 0.01- Если условие не выполняется — возвращает
nil.
- B. Рекурсивный опрос subviews: Если точка внутри, метод проходит по массиву
subviewsв обратном порядке (от самого верхнего к нижнему). Для каждогоsubview:- Конвертирует точку в систему координат subview.
- Рекурсивно вызывает у subview
hitTest(_:with:). - Если subview возвращает не-
nil(нашел свою цель), этот результат и возвращается.
- C. Возврат себя: Если точка внутри view, но ни один из subviews не вернул цель, то эта view и становится целевой. Метод возвращает
self.
- A. Проверка возможности быть целью: Вызывает
Упрощенная реализация (иллюстрирующая логику):
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:).