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

image


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

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

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

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

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

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

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

Всем процессом 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-ом.

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, with event: UIEvent?)
open func touchesMoved(_ touches: Set, with event: UIEvent?)
open func touchesEnded(_ touches: Set, with event: UIEvent?)
open func touchesCancelled(_ touches: Set, with event: UIEvent?)
open func pressesBegan(_ presses: Set, with event: UIPressesEvent?)
open func pressesChanged(_ presses: Set, with event: UIPressesEvent?)
open func pressesEnded(_ presses: Set, with event: UIPressesEvent?)
open func pressesCancelled(_ presses: Set, 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 может принимать следующие параметры:

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

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

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

© Habrahabr.ru