← Назад к вопросам

Что проверяет Responder Chain?

2.0 Middle🔥 181 комментариев
#UIKit и верстка

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Responder Chain в iOS

Что это такое

Responder Chain (цепь ответственности) — это механизм iOS для обработки событий, при котором события передаются от объекта к объекту вверх по иерархии, пока кто-то их не обработает. Это не проверка в классическом смысле, а маршрутизация событий.

Что именно проходит через Responder Chain

События, обрабатываемые через Responder Chain:

  1. Touch Events — касания пользователя (touchesBegan, touchesMoved, touchesEnded, touchesCancelled)
  2. Motion Events — движение устройства (встряска, наклон, ускорение)
  3. Remote Control Events — события с пульта (headphone buttons, Apple Watch)
  4. Custom Events — пользовательские события

События, НЕ используют Responder Chain:

  • Keyboard events (идут в first responder)
  • Accessibility events
  • Gesture Recognizers (создают события на основе touch)

Иерархия Responder Chain

// Стандартная иерархия представлений

Window
  └── View Controller's View
      └── Container View
          └── Button
              └── Label

// Responder Chain идет вверх:
// Label → Container View → View Controller's View → View Controller 
// → Window → Application → Application Delegate

Процесс поиска обработчика

// Пользователь касается Button

// Шаг 1: Hit Testing — определение целевого view
hitTest(_:with:) на Window
   hitTest(_:with:) на View Controller's View
     hitTest(_:with:) на Container View
       hitTest(_:with:) на Button (НАЙДЕН!)

// Шаг 2: Event Delivery — передача события
if Button.touchesBegan(touches, with: event) {
    // Button обработал событие — цепь останавливается
    // Вывод: event handled = true
} else {
    // Button не обработал — передаем дальше
    Button.superview?.touchesBegan(touches, with: event)
}

Hit Testing механизм

Прежде чем событие попадет в Responder Chain, система должна определить, который view его должен получить.

// Hit Testing проверяет две вещи:

class UIView {
    // 1. Видим ли view
    open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if self.isHidden { return nil }  // Скрытый view не может обработать
        if !self.isUserInteractionEnabled { return nil }  // Взаимодействие отключено
        if self.alpha < 0.01 { return nil }  // Полностью прозрачный
        
        // 2. Находится ли точка касания в границах view
        if self.point(inside: point, with: event) {
            // Пытаемся обработать
            for subview in self.subviews.reversed() {
                let location = self.convert(point, to: subview)
                if let hitView = subview.hitTest(location, with: event) {
                    return hitView  // Найдем самый глубокий view
                }
            }
            return self  // Или сам обработаю
        }
        return nil  // Не внутри границ
    }
    
    // Точная проверка: находится ли точка внутри
    open func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        return bounds.contains(point)
    }
}

Проверка в Responder Chain

// Каждый responder проверяет: Я могу это обработать?

class UIResponder {
    // Может ли этот responder обработать событие
    open func canPerformAction(
        _ action: Selector,
        withSender sender: Any?
    ) -> Bool {
        // Проверяет, есть ли метод для этого действия
        return self.responds(to: action)
    }
    
    // Следующий responder в цепи
    open var next: UIResponder? {
        // Для View -> superview
        // Для View Controller -> его view
        // Для Window -> Application
        return nil
    }
}

Пример полной обработки события

// Интерактивный пример

class TouchViewController: UIViewController {
    let button = UIButton()
    let label = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(button)
        button.addSubview(label)
    }
    
    // Responder Chain для касания кнопки:
    // Button -> View Controller -> Window -> Application
}

// 1. Пользователь касается Button
// 2. Hit Testing находит Button
// 3. Button.touchesBegan() вызывается

class CustomButton: UIButton {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("1. Button.touchesBegan() — обработал")
        // Если не вызовем super, событие здесь заканчивается
        super.touchesBegan(touches, with: event)
    }
}

class CustomViewController: UIViewController {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("2. ViewController.touchesBegan() — событие передалось вверх")
        super.touchesBegan(touches, with: event)
    }
}

class CustomApplication: UIApplication {
    override func sendEvent(_ event: UIEvent) {
        print("3. Application.sendEvent() — последняя точка")
        super.sendEvent(event)
    }
}

Что проверяет Responder Chain на каждом уровне

1. Может ли этот responder обработать это событие?

if self.responds(to: selector) {
    // Да, есть метод touchesBegan
    self.touchesBegan(touches, with: event)
} else {
    // Нет, передаем дальше
    self.next?.touchesBegan(touches, with: event)
}

2. Вызвал ли responder super?

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    // Если НЕ вызовем super — событие останавливается
    // Если вызовем super — передается дальше
    super.touchesBegan(touches, with: event)  // Передаем вверх
}

3. Есть ли next responder?

var responder: UIResponder? = startingResponder
while let current = responder {
    if current.responds(to: #selector(UIResponder.touchesBegan(_:with:))) {
        current.touchesBegan(touches, with: event)
    }
    responder = current.next  // Переходим к next responder
}

Практический сценарий

screenspace: Window
├── RootViewController
   ├── ParentView
      └── ChildView (касание здесь)
   └── OtherView

// Hit Testing:
Window.hitTest() -> RootViewController.view.hitTest() 
  -> ParentView.hitTest() -> ChildView.hitTest() 
  -> ChildView (НАЙДЕН)

// Responder Chain для ChildView:
ChildView.touchesBegan()
  -> (если не обработал или вызвал super)
  -> ParentView.touchesBegan()
  -> RootViewController.touchesBegan()
  -> Window.touchesBegan()
  -> Application.sendEvent()

Важные проверки

1. isUserInteractionEnabled — если false, view пропускается при hit testing

button.isUserInteractionEnabled = false  // Button не получит события

2. isHidden — скрытые view не участвуют в hit testing

button.isHidden = true  // Button не получит события

3. alpha < 0.01 — полностью прозрачные view пропускаются

button.alpha = 0  // Button не получит события

4. clipsToBounds — влияет на обработку событий подвьюх

containerView.clipsToBounds = true  // События за границами игнорируются

Кастомизация Responder Chain

class CustomResponder: UIResponder {
    override var next: UIResponder? {
        // Можно вставить кастомный responder
        return MyCustomResponder()
    }
}

// Или переопредели touchesBegan для специальной обработки
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    if shouldHandle(touches) {
        handleTouches(touches)
    } else {
        super.touchesBegan(touches, with: event)  // Передаем дальше
    }
}

Выводы

Responder Chain проверяет:

  • Видимость и интерактивность (hit testing)
  • Наличие обработчика события (responds to selector)
  • Согласие обработчика (вызов super или обработка)
  • Доступность next responder (цепь до Application)
Что проверяет Responder Chain? | PrepBro