Что такое responder chain в iOS и как он обрабатывает события
Что проверяют
Понимаете ли вы, как событие находит своего получателя в UIKit и почему кнопка иногда «не нажимается». Следом обычно спрашивают про hit-testing и про то, как отправить действие по цепочке, не зная получателя заранее.
Как отвечать
Responder chain это связанный список объектов-наследников UIResponder: UIView, UIViewController, UIWindow, UIApplication. У каждого есть свойство next, которое указывает на следующее звено. Правила такие:
- у view: если это корневая view контроллера, дальше идёт сам контроллер, иначе superview;
- у контроллера: superview его view, для корневого контроллера окно, для модально показанного презентующий контроллер;
- у окна: дальше
UIApplication, а после негоAppDelegate, если он наследуетUIResponder.
Для касаний важно разделять два прохода. Сначала hit-testing идёт сверху вниз: окно вызывает hitTest(_:with:) и point(inside:with:) у подвидов и находит самую глубокую view под пальцем. Скрытые view, а также view с isUserInteractionEnabled = false или alpha меньше 0.01 пропускаются. Найденная view получает touchesBegan(_:with:), и если не обрабатывает касание, реализация по умолчанию передаёт его вверх по next.
Для остальных событий (клавиатура, pressesBegan, shake, команды меню) стартовая точка это first responder: объект, который вызвал becomeFirstResponder() и вернул true из canBecomeFirstResponder.
Практическое применение: UIApplication.shared.sendAction(_:to:from:for:) с to: nil отправляет селектор по цепочке от first responder. Так скрывают клавиатуру через resignFirstResponder, а ячейка сообщает контроллеру о нажатии без делегата.
Где спотыкаются
Смешивают hit-testing и responder chain. Первый ищет получателя сверху вниз, вторая передаёт необработанное событие снизу вверх.
Не знают, почему кнопка за пределами bounds родителя не реагирует на нажатие. clipsToBounds = false рисует её, но point(inside:with:) родителя возвращает false, и hit-testing до неё не доходит.
Переопределяют touchesBegan без вызова super. Событие дальше не уходит, и родитель или контроллер его уже не получат.