HitTest and UIResponder in iOS

Mayur Kothawade
5 min readMar 25, 2021

Let’s start with an interesting problem and we can understand the concept in much better on the way of finding the ultimate solution for the problem.

Problem statement : Problem is User should be able to click on “Background Button” button.

Details: We have a following UI where the view hierarchy looks like bellow.

View Hierarchy
Visual debug mode

Here MainView has a RedView as subview which has 2 subviews named RedButton and ScrollView respectively, scrollview has its own content view (GreenView) which itself has top and bottom indicator labels.

UI Behaviour (GIF)

As we want green view to acquire fullscreen for scrolling, Scrollview beneath is already overlapping the “Background Button” (RedButton), so currently tapping on button won’t work as all taps are consumed by scrollview.

Easy Solution: Easiest solution is to disable the user interaction of the top view(scrollview) so that taps will be directly consumed by deep view (button).

But in such case we will miss the basic scrolling feature of the scrollview. So this solution will not be helpful.

So what to do? How to solve this?

As far as our knowledge is concern, when user taps on screen, UIKit will send events to only the top most views in hierarchy, and if it doesn’t responds then it goes the the next level of view where the tap belongs.

If above is know thing for you then congratulations! we almost aware of the concept, Just need to put it in better way.

So UIKit decides to whom to send the next event using UIResponder chain, UIResponder is a foundation of handling UI events in iOS, everything is UIResponder which can respond to the user interaction. UIResponder is a linked list maintain by UIKit to pass a responsibility within UIResponders for a particular hierarchy.

So in this example if we consider lets say GreenView, then its UIResponder chain would be GreenView->ScrollView->RedView->MainView->ViewController->UIWindow->AppDelegate

Responder chain for GreenView

User taps on GreenView, UIKit will pass the event in above sequence only starting from GreenView till AppDelegate until one of the UIResponder respond to that event.

Now question is how does UIKit know that user has tap on GreenView or ScrollView or label or something else…?

There comes the concept of hitTest, UIKit uses view based hitTest mechanism to identify which is the top most UIResponder to respond to particular event,
For that it uses an reverse pre-order depth-first traversal algorithm, possible implementation of algorithm in UIView might looks like below:

hitTest possible implementation

The hitTest:withEvent: method first checks if the view is allowed to receive the touch. A view is allowed to receive the touch if:

  • The view is not hidden:
    self.hidden == NO
  • The view has user interaction enabled:
    self.userInteractionEnabled == YES
  • The view has alpha level greater than 0.01:
    self.alpha > 0.01
  • The view contains the point:
    pointInside:withEvent: == YES

Then, if the view is allowed to receive the touch, this method traverses the receiver’s subtree by sending the hitTest:withEvent: message to each of its subview from last to first until one of them returns non nil value. The first non nil value returned by one of the subviews is the frontmost view under the touch-point and is returned by the receiver. If all receiver’s subviews returned nil or the receiver has no subviews the receiver returns itself.

Otherwise, if the view is not allowed to receive the touch, this method returns nil without traversing the receiver’s subtree at all. Therefore, the hit-test process may not visit all the views in the view hierarchy.

Now that we have understood how UIKit works, how it identifies/search a tapped view in view hierarchy, let’s think how we can use the same knowledge to solve our initially mentioned problem.

So what happens when user tries to tap on “Background Button” ?

Here UIKit will execute it’s hitTest to search for the frontmost view which can respond to the event, in this case its a ScrollView, as there is no gesture recogniser registered for this action, it will simply ignore the event.

What we can do to solve our problem is, hack the process of identifying the hitView,

Best way to do that by overriding the hitTest method on any view within UIResponder chain in hitTest as the other paths will be rejected by hitTest algorithm.

Hit test when try to tap “Background Button”(RedButton)

So here lets override the hitTest for RedView (As its one of type CustomView in path and also a root for both paths)

Consider RedButton has a tag 4456

hit test solution

here we convert the coordinate of the touch point into the respected coordinate system of the target view (which is RedButton), and if touch belongs to button then we return a button, so in UIKit hitTest algorithm what we did is hacked the process by allowing it to traverse to RedButton path instead of going towards scrollview path.

Now once hitTest finds the hitView (RedButton), It will try to find whether hitView responds to particular event or not, as we have already registered an action for even touchupInside on target ViewController, It will trigger an action. If touch doesn’t belongs to button then we allow the algorithm to traverse further deep to find the view in same path, because of this user can also interact/scroll with ScrollView.

Done ! problem solved now user can tap on background button as well as scroll overlay scrollview.

Final Solution (GIF)

Problem code and solution code can be found on https://github.com/mayurkothawade/iOS_Concepts/tree/main/UIResponderHitTest

As UI becomes complex, firm knowledge of basic concepts in any technology becomes necessary, Above article will help you understand the concept by example, Happy coding.

You may be interested in reading more stories….

https://mayurkothawade.medium.com/ios-14-app-tracking-transparency-everything-you-need-to-know-4d2b0d047199

--

--

Mayur Kothawade

Building Mobile App that challenges creativity and innovation.