Pull to refresh

iOS Responder Chain или Что спрашивают на собеседовании

Reading time4 min
Views50K

image


Какая разница между первым и вторым примером?

За что отвечает таргет?

В каком случае вызывается метод при нажатие кнопки?

TL;DR


При нажатии на кнопку наш метод вызывается в обоих случаях.


Только в первом примере UIKit попытается вызвать метод в назначенном таргете(у нас это ViewController). Будет краш, если этого метода не существует.


Во втором же примере используется iOS Responder Chain, UIKit будет искать самого ближнего UIResponder-a у которого есть данный метод. Краша не будет, если наш метод не найден.


UIViewController, UIView, UIApplication наследуют от UIResponder.


iOS Responder Chain и что под капотом


Всем процессом iOS Responder Chain занимается UIKit, который динамично работает со связным списком UIResponder-ов. Этот список UIKit создает из first responder(первый UIResponder который зарегистрировал событие, у нас это UIButton(UIView) и его subviews.


image


UIKit проходит через список UIResponder-ов и проверяет с помощью canPerformAction на наличие нашей функции.


open func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool

Если выбранный UIResponder не может работать с конкретным методом,
UIKit рекурсивно посылает действия к следующему UIResponder-у в списке с помощью метода target который возвращает следующего UIResponder-а.


open func target(forAction action: Selector, withSender sender: Any?) -> Any?

Этот процесс повторяется до тех пор, пока кто-то из UIResponder-ов сможет работать с нашим методом или список закончится и это событие система проигнорирует.


Во втором примере нажатия обработалось UIViewController-ом, но UIKit сначала отправил запрос к UIView так как он был first responder. У него не было нужного метода, поэтому UIKit перенаправил действия на следующего UIResponder-а в связном списке кем являлся UIViewController у которого был нужный метод.


В большинстве случаев iOS Responder Chain это простой связной список subviews, но его очередность можно изменить. Можно заставить UIResponder (becomeFirstResponder) стать
первым UIResponder и вернуть его к старой позиции с помощью resignFirstResponder. Это часто используется с UITextField для показа клавиатуры которая будет вызвана, только когда UITextField является first responder-ом.


iOS Responder Chain и UIEvent


The Responder Chain так же участвует при касаниях экрана, движениях, нажатиях. Когда система определяет какое-то события(touch, motion, remote-control, press), под капотом создается UIEvent и отправляется с помощью метода UIApplication.shared.sendEvent() к UIWindow. После получения события UIWindow определяет с помощью метода hitTest:withEvent к какому UIResponder данное событие принадлежит и назначает его first responder-ом. Дальше идет работа с связным списком UIResponder-ов описанная выше.


Что бы работать с системными UIEvent-ами, сабклассы UIResponder (UIViewController, UIView, UIApplication) могут переопределить данные методы:


open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
open func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesChanged(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesCancelled(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
open func motionCancelled(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
open func remoteControlReceived(with event: UIEvent?)

Не смотря что возможность наследовать и вызывать sendEvent в ручную присутствует, UIResponder не предназначен для этого. Это может создать много проблем с работой кастомных событий, которые могут привести к не понятным действиям вызванными случайным first responeder-ом который может отреагировать на ваше событие.


Чем это полезно, где использовать


Не взирая на то, что iOS Responder Chain полностью контролируется UIKit-ом, его можно использовать для решения проблемы делегирования/общения. UIResponder действия похоже на одноразовые NotificationCenter.default.post.


Возьмем пример, у нас есть рут UIViewController, который глубоко находится в стеке UINavigationController и нам нужно ему передать что произошло при нажатие кнопки на другом экране. Можно воспользоваться делагат паттерном или NotificationCenter.default.post, но довольно простой вариант это использования iOS Responder Chain.


button.addTarget(nil, action: #selector(RootVC.doSomething), for: .touchUpInside)

При нажатие будет вызываться метод в рут UIViewController. #selector может принимать следующие параметры:


@objc func doSomething()
@objc func doSomething(sender: Any?)
@objc func doSomething(sender: Any?, event: UIEvent?)

sender это объект который отправил событие — UIButton, UITextField и так далее.


Дополнительные ресурсы для изучения [eng]:


Хорошое описание UIEvent, UIResponder и пару продвинутых примеров(координатор патерн)
Подробная статья о ios responder chain
Пример responder chain на практике
Офф дока по iOS responder chain
Офф дока по UIResponder

Tags:
Hubs:
Total votes 14: ↑14 and ↓0+14
Comments3

Articles