iOS Touch Event
1. user made a touch
2. system creates an event object with global coordinate of touch
3. hit testing by coordinate - find First Responder
4.1 send Touch Event to `UIGestureRecognizer`. After handling the touch can or can not(depends on setup) be forward to the First Responder
4.2 send Touch Event to the First Responder
4.2.1 handle Touch Event
Class diagram
3 Hit Testing
Hit Testing to find a First Responder - checking hierarchy of UIView and successors of it (e.g. UIWindow). Recursively find a front view starting from the biggests(back) UIView(UIWindow is a start/root point) to the smallest(front)) UIView. As a result First Responder
in this case is a top (the smallest) UIView
, point()
method(hitTest()
uses point()
internally) of which returned true
.
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
func point(inside point: CGPoint, with event: UIEvent?) -> Bool
Internally hitTest()
takes into account
point() == true
point()
takes into account rectangle which is inside superview
isUserInteractionEnabled == true
isHidden == false
func hitTest() -> View? {
if (isUserInteractionEnabled == false || isHidden == true || alpha == 0 || point() == false) { return nil }
for subview in subviews {
if subview.hitTest() != nil {
return subview
}
}
return nil
}
Notes:
- by default when you set
view.isUserInteractionEnabled = false
this view and all it's subview will not handle the touch event
- by default
point()
takes into account rectangle which lay into superview. It means that if a touch is occured on a view part which is drawn out of superview, this view and all it's subview will not handle the touch event
[UIView.clipsToBounds]
4 Send UIEvent
UIKit
creates UIEvent
which is sent by UIApplication.shared.sendEvent()
to main event loop
[About]. UIEvent contains one or more
UITouchwhich contains -
UIView`, location...
It always go through UIApplication.sendEvent() -> UIWindow.sendEvent() -> <First_Responder>
UIApplication.sendEvent() -> UIWindow.sendEvent() -> <First_Responder>.touchesBegan()
UIApplication.sendEvent() -> UIWindow.sendEvent() -> <First_Responder>.touchesEnded()
//UIApplication.shared.sendEvent()
//UIApplication, UIWindow
func sendEvent(_ event: UIEvent)
4.1 send Touch Event to UIGestureRecognizer
It is a simple and handy way to work with gestures. There are some out of box UIGestureRecognizer
like UITapGestureRecognizer
, UISwipeGestureRecognizer
... and you are able to create your own
let tap_v_0_2 = UITapGestureRecognizer(target: self, action: #selector(self.onView_0_2))
view_0_2.addGestureRecognizer(tap_v_0_2)
@objc func onView_0_2() {
print("---> onView V_0_2")
}
System tries to find a UIGestureRecognizer
in a view hierarchy. Starts from first responder (using UIView.superview
) up toUIWindow
(subclass of UIView
). It means if you setup UIGestureRecognizer
on UIWindow
and first responder is UIView_0
without UIGestureRecognizer
- UIWindow.UIGestureRecognizer
will be called
There are some functions to hadle work between UIGestureRecognizer
in UIGestureRecognizerDelegate
And some functions to handle forwarding event to Responder Chain like: cancelsTouchesInView
, delaysTouchesBegan
, delaysTouchesEnded
4.2 Send Touch Event to the First Responder
It is more low level and more advanced approach where you can customize your logic. To use it you should inherit from UIView
and override some methods, like:
//UIResponder
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
When UIView_0_2 is a first responder:
4.2.1 Handle Touch Event
When FirstRessponder is found it's a time to handle Touch Event using Responder Chain
Responder Chain
It a kind of chain of responsibility
pattern who extends UIResponder
. Every UIResponder
has next
property which points on next responder in the chain
By default:
UIView.next
-> superview ?? UIViewController
UIViewController.next
-> UINavigationController
?? UIWindow
UIWindow.next
-> UIApplication
Abstract diagram:
Print responder chain starting from UIView_0_2:
UIView_0_2 -> UIView_0 -> ViewController -> UIDropShadowView -> UITransitionView -> CustomWindow -> UIWindowScene -> CustomApplication -> AppDelegate
In case with Touch Event:
- it starts from FirstResponder and try to find overrode appropriate method (e.g.
touchesBegan()
...) If this method was not overrode - UIResponder.next
is used to find next responder and try to call this method there
- if you call
super.touchesBegan()
inside override func touchesBegan()
- UIResponder.next
is used to find next responder and try to call this method there
For example if first responder is UIView_0_2 but touchesBegan() is override in UIWindow(not in UIView_0_2) - UIWindow will hadle this touch event
UIWindow hitTest BEGIN
UIWindow point result:true
UIView_0 hitTest BEGIN
UIView_0 point result:true
UIView_0_1 hitTest BEGIN
UIView_0_1 point result:false
UIView_0_1 hitTest END result: nil
UIView_0_2 hitTest BEGIN
UIView_0_2 point result:true
UIView_0_2_1 hitTest BEGIN
UIView_0_2_1 hitTest END result: nil
UIView_0_2 hitTest END result: UIView_0_2
UIView_0 hitTest END result: UIView_0_2
UIWindow hitTest END result: UIView_0_2
Let's take a look at example:
Additional usage of Responder Chain
Responder chain
is also used by UIControl.addTarget()
or UIApplication.sendAction()
approaches like event bus.
customButton.addTarget(nil, action: #selector(CustomApplication.onButton), for: .touchUpInside)
//target is nil. In other cases Responder Chain is not ussed
UIApplication.shared.sendAction(#selector(CustomApplication.foo), to: nil, from: self, for: nil)
Internally it uses UIResponder.target()
and UIResponder.canPerformAction()
. When first UIRessponder starts a flow - target()
is called, if super.target()
is called inside override func target()
then canPerformAction()
is called, if canPerformAction()
returns false then next
is used to find a next UIResponder and recursively repeated these steps, if canPerformAction()
returns true(by default the selector was found in this object) then this target will be recursively returned back and will be used to call the selector
Notes:
- When target doesn't contain a selector but
canPerformAction()
returns true next error is thrown:
unrecognized selector sent to instance
For example - you call UIApplication.shared.sendAction(#selector(CustomApplication.foo), to: nil, from: self, for: nil)
from UIViewController
and(as you can see) foo
selector is located in CustomApplication
(UIApplication
)
UIViewController target Begin
UIViewController canPerformAction result:false
UIViewController next is: UIWindow
UIWindow target Begin
UIWindow canPerformAction result:false
UIWindow next is UIApplication
UIApplication target begin
UIApplication canPerformAction result:true
UIApplication target result: UIApplication
UIWindow target result: UIApplication
UIViewController target result: UIApplication
//foo method body logic
[Android onTouch]