Как система определяет, в какую вьюху попал касание?
Sobes Copilot
Получайте ответы в реальном времени
Ответ от нейросети
sobes.tech AI
Система использует два ключевых механизма: хит-тестирование (hit-testing) и иерархию вьюх.
-
Хит-тестирование (Hit-testing): Фреймворк UIKit использует метод
hitTest(_:with:)корневой вьюхи окна (UIWindow) для определения, какая вьюха находится под точкой касания. Этот метод рекурсивно обходит иерархию вьюх, начиная с корневой. -
Иерархия вьюх:
- Метод
hitTest(_:with:)сначала проверяет, находится ли точка касания внутри границ текущей вьюхи. Если нет, метод возвращаетnil. - Если точка внутри границ, метод просматривает дочерние вьюхи в обратном порядке (от последней добавленной к первой). Для каждой дочерней вьюхи вызывается ее собственный метод
hitTest(_:with:)с той же точкой касания (преобразованной в координаты дочерней вьюхи). - Первая дочерняя вьюха, чей метод
hitTest(_:with:)возвращает не-nilрезультат (т.е. она или одна из ее дочерних вьюх содержит точку), считается "попавшей" и этот результат возвращается вверх по иерархии. - Если ни одна дочерняя вьюха не содержит точку касания, а текущая вьюха сама содержит точку и не имеет
userInteractionEnabled = false, то система считает, что касание произошло в текущей вьюхе, и метод возвращаетself. - Если
userInteractionEnabledтекущей вьюхи равноfalseилиhiddenравноtrue, илиalphaменьше 0.01, метод возвращаетnil, даже если точка внутри границ.
- Метод
Процесс продолжается вниз по иерархии, пока не будет найдена самая нижняя в иерархии вьюха, содержащая точку касания. Эта вьюха становится hit-test view или hit-test result, и именно ей (или ее ближайшему подходящему предку) будет делегировано дальнейшее управление событием (методы touchesBegan, touchesMoved и т.д.).
Вот как можно переопределить hitTest или point(inside:with:) для кастомного поведения хит-тестирования:
import UIKit
class CustomView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// Проверяем, находится ли точка внутри нашей вьюхи.
// Если нет, возвращаем nil.
if !self.point(inside: point, with: event) {
return nil
}
// Проходимся по дочерним вьюхам в обратном порядке добавления.
for subview in subviews.reversed() {
// Преобразуем точку из координат родительской вьюхи в координаты дочерней.
let convertedPoint = subview.convert(point, from: self)
// Рекурсивно вызываем hitTest для дочерней вьюхи.
if let hitTestView = subview.hitTest(convertedPoint, with: event) {
// Если дочерняя вьюха или ее потомки содержат точку, возвращаем результат.
return hitTestView
}
}
// Если никто из дочерних вьюх не обработал касание,
// а текущая вьюха интерактивна и contient point, возвращаем self.
// Базовая реализация point(inside:with:) проверяет bounds и userInteractionEnabled.
return self
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// Базовая реализация проверяет, находится ли точка внутри bounds вьюхи
// и активна ли вьюха (userInteractionEnabled, alpha > 0.01, hidden == false).
// return super.point(inside: point, with: event)
// Можно переопределить для кастомных форм или отступов, например, увеличить область касания
let extendedBounds = bounds.insetBy(dx: -20, dy: -20) // Увеличиваем область на 20pts со всех сторон
return extendedBounds.contains(point)
}
}
Метод point(inside:with:) вызывается методом hitTest(_:with:) для быстрой проверки, находится ли точка вообще внутри области вьюхи, прежде чем проверять дочерние вьюхи.
В итоге находится именно самая нижняя интерактивная вьюха в иерархии, чья геометрия содержит точку касания и у которой userInteractionEnabled равен true, hidden — false и alpha больше 0.01.