Назад к вопросам
Middle
80
questionbank

Как система определяет, в какую вьюху попал касание?

Sobes Copilot

Получайте ответы в реальном времени

Ответ от нейросети

sobes.tech AI

Система использует два ключевых механизма: хит-тестирование (hit-testing) и иерархию вьюх.

  1. Хит-тестирование (Hit-testing): Фреймворк UIKit использует метод hitTest(_:with:) корневой вьюхи окна (UIWindow) для определения, какая вьюха находится под точкой касания. Этот метод рекурсивно обходит иерархию вьюх, начиная с корневой.

  2. Иерархия вьюх:

    • Метод 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, hiddenfalse и alpha больше 0.01.