Как переопределить hitTest(_:with:) в UIKit?

Ответ

Да, метод hitTest(_:with:) в UIView можно переопределить для кастомной логики определения, какое view должно получить касание.

Типичные сценарии использования:

  1. Расширение или сужение области касания.
  2. Игнорирование касаний для определенных subviews.
  3. Перенаправление событий.

Пример: расширение зоны касания

class TouchExtendedView: UIView {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // 1. Стандартные проверки (обязательно)
        guard self.isUserInteractionEnabled,
              !self.isHidden,
              self.alpha > 0.01,
              self.point(inside: point, with: event) else {
            return nil
        }

        // 2. Расширяем область проверки на 10 пунктов
        let expandedBounds = self.bounds.insetBy(dx: -10, dy: -10)
        guard expandedBounds.contains(point) else {
            return nil
        }

        // 3. Рекурсивная проверка дочерних view (стандартное поведение)
        for subview in subviews.reversed() {
            let convertedPoint = subview.convert(point, from: self)
            if let hitView = subview.hitTest(convertedPoint, with: event) {
                return hitView
            }
        }

        // 4. Если дочерние view не обработали, возвращаем self
        return self
    }
}

Best practices:

  • Всегда выполняйте базовые проверки (isUserInteractionEnabled, isHidden, alpha).
  • Для сложной геометрии используйте point(inside:with:).
  • Проверяйте дочерние view в обратном порядке (reversed()), так как они отрисовываются поверх друг друга.
  • Избегайте тяжелых вычислений внутри метода, так как он вызывается часто.

Ответ 18+ 🔞

О, слушай, смотри, вот эта штука hitTest(_:with:) в UIView — это же, блядь, прям магия какая-то, если её правильно переопределить. Можно творить, что хочешь, с этими касаниями, как будто ты шаман какой-то, а не разработчик.

Зачем это вообще, нахуй, нужно? Ну, например:

  1. Сделать кнопку, которую можно тыкать не только по самой кнопке, а ещё и за её пределы на 10 пикселей, потому что пальцы у пользователей, как сосиски, блядь.
  2. Игнорировать касания на каких-нибудь декоративных вьюхах, чтобы они не перехватывали события, как хитрая жопа.
  3. Перенаправить касание с одной вьюхи на другую, потому что логика у тебя там, ёпта, хитрая.

Вот смотри, как сделать кнопку с увеличенной зоной тыканья:

class TouchExtendedView: UIView {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // 1. Сначала стандартные проверки, без них нихуя не работает
        guard self.isUserInteractionEnabled,
              !self.isHidden,
              self.alpha > 0.01,
              self.point(inside: point, with: event) else {
            return nil // Если вьюха спрятана или выключена — пошёл нахуй, касание
        }

        // 2. А вот тут магия: расширяем зону проверки на 10 пунктов во все стороны
        let expandedBounds = self.bounds.insetBy(dx: -10, dy: -10)
        guard expandedBounds.contains(point) else {
            return nil // Ткнул мимо расширенной зоны? Иди нахуй.
        }

        // 3. Теперь смотрим на детей (subviews), кто из них ближе к экрану
        for subview in subviews.reversed() { // reversed() — потому что верхние вьюхи рисуются поверх
            let convertedPoint = subview.convert(point, from: self)
            if let hitView = subview.hitTest(convertedPoint, with: event) {
                return hitView // Нашли, кто должен получить касание? Отлично, вот он.
            }
        }

        // 4. Если дети отказались — тогда касание получаем мы сами
        return self
    }
}

Важные моменты, чтобы не обосраться:

  • Эти базовые проверки в guard — это святое, блядь. Без них можешь получить поведение, от которого волосы дыбом встанут.
  • Метод point(inside:with:) — твой друг, если форма вьюхи не прямоугольная, а какая-нибудь ебаная скруглённая или вообще в виде звезды.
  • subviews.reversed() — это важно, ёпта! Вьюхи, которые сверху (позже добавленные), должны проверяться первыми, логично же?
  • Не устраивай тут, блядь, вычисления вселенной внутри этого метода. Он вызывается постоянно, как сумасшедший, на каждый чих системы. Сделал его тяжёлым — и приложение будет тормозить, как черепаха в сиропе.