Какая разница между первым и вторым примером?
За что отвечает таргет?
В каком случае вызывается метод при нажатие кнопки?
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
.
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