Hit test проходит рекурсивно или итеративно?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Рекурсия или итерация в hitTest
В iOS при обработке касаний система использует рекурсивный подход для выполнения hitTest. Это фундаментальный механизм, который работает сверху вниз по иерархии представлений (UIView), начиная от корневого окна (UIWindow) и до самого глубокого дочернего представления.
Как работает рекурсивный hitTest
Основной метод hitTest(_:with:) класса UIView реализован рекурсивно. Вот его упрощённая логика:
-
Проверка условий: Метод сначала проверяет, может ли представление участвовать в hit-тестировании:
isUserInteractionEnabled == trueisHidden == falsealpha > 0.01Если любое условие не выполняется, метод возвращаетnil.
-
Проверка попадания в границы: Метод проверяет, находится ли точка касания внутри границ представления с помощью
point(inside:with:):if !self.point(inside: point, with: event) { return nil }Если точка вне границ, возвращается
nil. -
Рекурсивный обход дочерних представлений: Если точка внутри границ, система начинает рекурсивно проверять дочерние представления в обратном порядке Z-индекса (от последнего добавленного к первому):
for subview in subviews.reversed() { let convertedPoint = subview.convert(point, from: self) if let hitView = subview.hitTest(convertedPoint, with: event) { return hitView } }Этот цикл вызывает
hitTestдля каждого дочернего представления, передавая преобразованную точку. -
Возврат результата: Если ни одно дочернее представление не вернуло результат (точка не попала в них или они не участвуют в hit-тестировании), метод возвращает
self— текущее представление становится hit-представлением.
Пример рекурсивной цепочки
Рассмотрим иерархию:
UIWindow
└── ContainerView (alpha: 1.0, userInteractionEnabled: true)
├── Button (frame: (20, 20, 100, 50))
└── Label (frame: (150, 20, 100, 50))
При касании в точке (40, 30):
hitTestвызывается дляUIWindow.UIWindowпроверяет попадание в границы → точка внутри.UIWindowрекурсивно вызываетhitTestдляContainerView(его единственного дочернего).ContainerViewпроверяет попадание → точка внутри.ContainerViewв циклеfor subview in subviews.reversed():- Сначала проверяет
Label→ точка вне границ →nil. - Затем проверяет
Button→ точка внутри границ →ButtonвызываетhitTestдля своих дочерних (их нет) → возвращаетself.
- Сначала проверяет
- Результат (
Button) передаётся вверх по цепочке вызовов.
Почему рекурсия, а не итерация?
- Естественное соответствие иерархии представлений: Рекурсия идеально отражает древовидную структуру UIView, где каждое представление может иметь дочерние.
- Простота реализации и понимания: Рекурсивный алгоритм более интуитивно понятен для обхода деревьев.
- Гибкость переопределения: Разработчики могут переопределять
hitTest(_:with:)в подклассах UIView, добавляя кастомную логику (например, расширение области касания), и при этом сохраняется естественный flow. - Эффективность для типичных сценариев: Глубина иерархии представлений в iOS редко превышает 10-15 уровней, поэтому риск переполнения стека минимален.
Важные нюансы
- Оптимизация: Хотя алгоритм рекурсивный, система iOS может оптимизировать его выполнение, особенно при обработке множественных касаний или в сложных анимациях.
- Переопределение метода: При кастомной реализации важно сохранять базовую логику, иначе можно нарушить всю цепочку обработки событий:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { // Кастомная логика, например, расширение hit-области let expandedRect = bounds.insetBy(dx: -10, dy: -10) if expandedRect.contains(point) { return super.hitTest(point, with: event) } return nil } - Производительность: Слишком глубокая иерархия представлений или сложная логика в
hitTestможет повлиять на отзывчивость интерфейса. В таких случаях рекомендуется упрощать структуру или кэшировать результаты.
Таким образом, hitTest в iOS действительно реализован рекурсивно, что является наиболее естественным и эффективным подходом для обхода иерархии представлений и определения целевого объекта для событий касания.