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

в 6:11, , рубрики: ios app development, ios interview, swift, разработка мобильных приложений, разработка под iOS

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 может принимать следующие параметры:

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

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

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

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

Автор: Анатолий Касьянов

Источник