Imagine that you are a mobile phone 📟. When someone touches the screen, you need to find out what he touched. He may touch a button, or a list, or it may be an accidental touch. If two buttons overlap, or if you need to drag a button on a scrolllist, will your mechanism work? In iOS, the system has designed a set of solutions for us through UIKit, which is also the content of this article: event transmission and response chain mechanism in iOS.
Who will respond to the event
In UIKit we use a Responder object to receive and handle events. A responder object is usually an instance of UIResponder class, and its common subclasses include UIView, UIViewController, and UIApplication, which means that almost any control we use everyday is a responder, such as UIButton, UILabel, etc.
In UIResponder and its subclasses, we handle and pass events (UIEvent) via UITouch methods, as follows:
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?).
Copy the code
UIResponder can also handle UIPress, accelerometer, remote control events, and I’m only talking about touch events here.
A large amount of data is stored on the UITouch, and the UITouch data is updated as the finger moves across the screen. For example, which window or view did the touch occur in? What are the coordinates of the current touch point? What are the coordinates of the previous touch point? What is the status of the current touch event? These are stored on the UITouch. It is also important to note that in the arguments to these four methods, a set of UITouch types is passed (not a Single UITouch), which corresponds to two or more fingers touching the same view.
Identify the first responder
When someone touches the screen, we need to find out what the user touched, or we need to find out, after that touch, which control the user most wants to initiate a response. The process is to determine who is the first responder to the touch event.
After a touch occurs, UIApplication will trigger func sendEvent(_ event: UIEvent) passes a wrapped UIEvent to the UIWindow, which is the UIWindow that’s currently showing, which usually passes to the UIViewController that’s currently showing, which then passes to the root view of the UIViewController. This process is a one-stop service, no bifurcation. But once passed to the root view of the current UIViewController, which is the main battlefield for developers, the hierarchy of views can become intricate.
Here we use UIView as the main component of the view hierarchy, just to make it easy to understand. But it’s not just UIView that can respond to events, it’s actually any subclass of UIResponder that can respond to and pass events. We’ll use a lot of views or UIView examples later, but it really means a qualified responder.
Back to the original question, I am now a mobile phone 📱, and I know that someone has touched the screen. The information I have is the coordinates of the touch point, which I know should be one of the views in the hierarchy, but I can’t directly tell which view the user wants to click on. I need a strategy to find this first responder. UIKit provides us with hit-testing to determine the responder of the touch event. The strategy works like this:
Hit testing
There are a few more details in the picture that need to be explained first:
- in
Check whether you can receive events
The view cannot receive events if it meets any of the following three conditions:view.isUserInteractionEnabled = false
The alpha < = 0.01
view.isHidden = true
Check that the coordinates are inside themselves
This process is usedfunc point(inside point: CGPoint, with event: UIEvent?) -> Bool
Method to determine whether a coordinate is inside itself. This method can be overridden.Repeat through the child views from back to front
It means according toFILO
, and all its subviews are hit tested according to the added after traversal rule. This rule ensures that the last view added to the hierarchy of views is tested first, and if there is overlap between views, that view is the most complete view of its peers, i.e. the one the user is most likely to want to highlight.- in
Look at the horizontal sibling view in order
If there are no unexamined views left, you should go toAye? No subviews fit the bill?
.
To illustrate this process, let’s use an example where we start the process from the root view of the current UIViewController. The gray view A in the following figure can be regarded as the root view of the current UIViewController, and the hierarchy of each view is shown on the right. The user’s touch point on the screen is 🌟, and these 5 views can receive events normally. ⚠️ And notice that D is added to A later than B.
The specific process is as follows:
- First, A is hit tested, obviously 🌟 is inside A, and then check whether A has subviews according to the process.
- We see that A has two subviews, so we need to press
FILO
In principle, the subview is traversed, and the hit test is first performed on D and then on B. - We hit test D, and we find that 🌟 is not inside D, which means THAT D and its subviews must not be the first responder.
- Following the sequence of hit tests on B, we find that 🌟 is inside B, and follow the process to check whether B has subviews.
- We find that B has a subview C, so we need to hit test C.
- Obviously 🌟 is not inside C, so we have this information: the touch point is inside B, but not in any of B’s subviews.
- Conclusion: B is the first responder, and the hit test is finished.
- The whole hit test goes like this: A✅ –> D❎ –> B✅ –> C❎ >>>>B
The whole process should be clear 🐶, in fact, the process is a METHOD of UIView: func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? The UIView that the method returns at the end? That is, the first responder. The method code restore should look like this:
class HitTestExampleView: UIView {
override func hitTest(_ point: CGPoint.with event: UIEvent?). -> UIView? {
if !isUserInteractionEnabled || isHidden || alpha < = 0.01 {
return nil // The view cannot accept events
}
if self.point(inside: point, with: event) { // Determine whether the touch point is inside itself
for subview in subviews.reversed() { // Use FILO to traverse the child view
let convertedPoint = subview.convert(point, from: self)
let resultView = subview.hitTest(convertedPoint, with: event)
If the touch point is inside the subview, return the view. If it is not, return nil
if resultView ! = nil { return resultView }
}
return self All subviews of the view are not compliant, and the touch point is inside the view itself
}
return nil // This refers to whether the touch point is not inside the view}}Copy the code
Cross the line!
As an additional example of this process, if you look at the view hierarchy and touch points below, the final first responder is still B, and even the whole hit test goes the same way as before: A✅ –> D❎ –> B✅ –> C❎ >>>> B, A✅ –> D❎ –> B✅ –> C❎ >>>> B, A✅ –> D❎ –> B✅ –> C❎ >>>> B This example tells us to pay attention to whether clickable subviews are outside the scope of the superview. Func point(inside point: CGPoint, with event: UIEvent?) -> Bool method to expand the click range. For this approach, personally, it is ok, but not necessary, to seek reasonable view layout and clear and readable code than this key 💪.
Events are passed through the response chain
Identify the response chain members
After the first responder is found, the entire response chain is determined. The so-called response chain is a linked list composed of responders, the head of the list is the first responder, and the next node of each node of the list is the next attribute of the node.
The response chain is the path that goes through in a hit test. For the example in the previous section, the trend of the whole hit test is: A✅ –> D❎ –> B✅ –> C❎. We remove the ❎ that is not passed, and take the first responder B as the head, and connect in sequence. The response chain is: B -> A. (In fact, A is followed by A controller, etc., but in this example, the controller is not shown, so write A.)
By default, if the node is of TYPE UIView, this next property is the superview of that node. But there are a few exceptions:
- If it is
UIViewController
The next responder isUIViewController
. - If it is
UIViewController
- if
UIViewController
The view isUIWindow
The next responder isUIWindow
Object. - if
UIViewController
Is by anotherUIViewController
The next responder is the second responderUIViewController
.
- if
UIWindow
The next responder ofUIApplication
.UIApplication
The next responder ofapp delegate
. But only if theapp delegate
是UIResponder
Is not an instance ofUIView
,UIViewController
Or the App object itself, is the next responder.
Here’s an example. As shown in the figure below, the touch point is 🌟, and according to the hit test, B becomes the first responder. Since C is the superview of B, A is the superview of C, and A is the root view of Controller, the response chain looks like this:
View B -> ViewC -> Root view A -> UIViewController object -> UIWindow object -> UIApplication object -> App Delegate
The light gray arrow in the figure refers to adding UIView directly to UIWindow.
Pass events along the response chain
The event will first be responded to by the first responder, causing it to open func Touches began (_ Touches: Set
, with event: Touches?). And other methods, according to the way of touch (such as dragging, two fingers), the specific method and process are not the same. If the first responder does not process the event in this method, it is passed to the next responder in the response chain to trigger the method processing, and if the next responder does not process the event, and so on. If no one responds at the end, it is discarded (such as a mistouch). We can create a subclass of UIView and add some printing functions to see how the response chain works.
class TouchesExampleView: UIView {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?). {
print("Touches Began on " + colorBlock)
super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?). {
print("Touches Moved on " + colorBlock)
super.touchesMoved(touches, with: event)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?). {
print("Touches Ended on " + colorBlock)
super.touchesEnded(touches, with: event)
}
}
Copy the code
Here’s an example. As shown in the figure below, A, B and C are UIViews. We move our finger A certain distance on the screen according to the position of 🌟 and the direction of the arrow, and then release the hand. We should be able to see the output shown on the lower right in the console. We can see that view A, view B and view C respond positively to each event. After each touch occurs, B’s response method is triggered first, then passed to C, and then passed to A. But this “positive” response means that in our case, neither A, B, or C are appropriate recipients of the touch event. They “actively” pass on the event because they look at the information about the event and decide that they are not the right person to handle it. (Of course, we have three UIViews here, and they shouldn’t be able to handle events themselves.)
So how does the console print if we replace the C in the figure above with the UIControl class we normally use? As shown in the lower right, the response chain stops at C, where A’s touches method is not touched. This means that in the response chain, UIControl and its subclasses, by default, do not pass events. In code, you can understand that UIView defaults to calling its next touches method inside its touches method, while UIControl defaults to not. With this done, when a control receives an event, the passing of the event is terminated. In addition, UIScrollView also works in this way.
The mechanism by which UIControl receives information is a target-action mechanism, which is related to, but not exactly the same as, UIGestureRecognizer. The differences and connections will be discussed in the next article on responding to the Chain X gesture.
Of course, we can actually inherit UIView and make a View that handles events and continues to pass events. You can do the same by inheriting UIControl and triggering next’s corresponding touches at the right time. Just think about ⚠️🍄 before you do it. Do you really want to issue an event to multiple controls? Is the effect correct when the hierarchy of controls is rearranged? Are you just trying to make a scene? Wait for a problem.
conclusion
In general, the transmission of events behind the screen can be divided into the following steps:
- Find the “first responder” with a “hit test”
- The “response chain” is determined by the first responder
- Events are passed along the response chain
- The event is either received by a responder or discarded by no responder
These steps are based on not using UIGestureRecognizer, and the next article will cover the case for responding to the chain X gesture.