155

While most apple documents are very well written, I think 'Event Handling Guide for iOS' is an exception. It's hard for me to clearly understand what's been described there.

The document says,

In hit-testing, a window calls hitTest:withEvent: on the top-most view of the view hierarchy; this method proceeds by recursively calling pointInside:withEvent: on each view in the view hierarchy that returns YES, proceeding down the hierarchy until it finds the subview within whose bounds the touch took place. That view becomes the hit-test view.

So is it like that only hitTest:withEvent: of the top-most view is called by the system, which calls pointInside:withEvent: of all of subviews, and if the return from a specific subview is YES, then calls pointInside:withEvent: of that subview's subclasses?

Cœur
  • 37,241
  • 25
  • 195
  • 267
realstuff02
  • 1,645
  • 4
  • 12
  • 6
  • 4
    A very good tutorial that helped me out [link](http://smnh.me/hit-testing-in-ios) – anneblue Dec 17 '15 at 15:07
  • The equivalent newer document for this might now be https://developer.apple.com/documentation/uikit/uiview/1622469-hittest – Cœur Mar 24 '19 at 17:51

7 Answers7

305

I think you are confusing subclassing with the view hierarchy. What the doc says is as follows. Say you have this view hierarchy. By hierarchy I'm not talking about class hierarchy, but views within views hierarchy, as follows:

+----------------------------+
|A                           |
|+--------+   +------------+ |
||B       |   |C           | |
||        |   |+----------+| |
|+--------+   ||D         || |
|             |+----------+| |
|             +------------+ |
+----------------------------+

Say you put your finger inside D. Here's what will happen:

  1. hitTest:withEvent: is called on A, the top-most view of the view hierarchy.
  2. pointInside:withEvent: is called recursively on each view.
    1. pointInside:withEvent: is called on A, and returns YES
    2. pointInside:withEvent: is called on B, and returns NO
    3. pointInside:withEvent: is called on C, and returns YES
    4. pointInside:withEvent: is called on D, and returns YES
  3. On the views that returned YES, it will look down on the hierarchy to see the subview where the touch took place. In this case, from A, C and D, it will be D.
  4. D will be the hit-test view
pgb
  • 24,813
  • 12
  • 83
  • 113
  • Thank you for the answer. What you described is also what was in my mind, but @MHC says `hitTest:withEvent:` of B, C and D are also invoked. What happens if D is a subview of C, not A? I think I got confused... – realstuff02 Feb 10 '11 at 19:20
  • 2
    In my drawing, D is a subview of C. – pgb Feb 10 '11 at 19:34
  • 1
    Wouldn't `A` return `YES` as well, just as `C` and `D` does? – Martin Wickman Jun 14 '11 at 21:57
  • 2
    Don't forget that views that are invisible (either by .hidden or opacity below 0.1), or have user interaction turned off will never respond to hitTest. I don't think hitTest is being called on these objects in the first place. – Jonny May 03 '13 at 03:23
  • Just wanted to add that hitTest:withEvent: may be called on all the views depending on their hierarchy. – Adithya Sep 02 '13 at 12:11
  • I want to say that, you are wrong. If you define a custom subclass of uiview, you'll notice that no one calls your pointInside. the `hitTest` is the only method that is called by your parent view. and in your implementation of hitTest, you don't call subview's pointInside, you just call hitTest. – lastcc Jan 19 '17 at 13:54
179

It seems quite a basic question. But I agree with you the document is not as clear as other documents, so here is my answer.

The implementation of hitTest:withEvent: in UIResponder does the following:

  • It calls pointInside:withEvent: of self
  • If the return is NO, hitTest:withEvent: returns nil. the end of the story.
  • If the return is YES, it sends hitTest:withEvent: messages to its subviews. it starts from the top-level subview, and continues to other views until a subview returns a non-nil object, or all subviews receive the message.
  • If a subview returns a non-nil object in the first time, the first hitTest:withEvent: returns that object. the end of the story.
  • If no subview returns a non-nil object, the first hitTest:withEvent: returns self

This process repeats recursively, so normally the leaf view of the view hierarchy is returned eventually.

However, you might override hitTest:withEvent to do something differently. In many cases, overriding pointInside:withEvent: is simpler and still provides enough options to tweak event handling in your application.

MHC
  • 6,405
  • 2
  • 25
  • 26
  • Do you mean `hitTest:withEvent:` of all subviews are executed eventually? – realstuff02 Feb 10 '11 at 19:21
  • 3
    Yes. Just override `hitTest:withEvent:` in your views (and `pointInside` if you want), print a log and call `[super hitTest...` to find out whose `hitTest:withEvent:` is called in which order. – MHC Feb 10 '11 at 20:01
  • shouldn't step 3 where you mention "If the return is YES, it sends hitTest:withEvent: ...shouldn't it be pointInside:withEvent? I thought it sends pointInside to all subviews? – prostock May 12 '11 at 00:58
  • Back in February it first sent hitTest:withEvent:, in which a pointInside:withEvent: was sent to itself. I haven't re-checked this behavior with following SDK versions, but I think sending hitTest:withEvent: makes more sense because it provides a higher-level control of whether an event belongs to a view or not; pointInside:withEvent: tells whether the event location is on the view or not, not whether the event belongs to the view. For example, a subview may not want to handle an event even if its location is on the subview. – MHC May 18 '11 at 21:49
  • I think, it also be consider the zOrder of subviews. the enumeration subviews might be from top zOrder to lower. If two view belongs to the some parent view. The two views intersect each other. The one at to top will be first called. – Xiangping Meng Sep 10 '12 at 10:16
  • 1
    WWDC2014 Session 235 - Advanced Scrollviews and Touch Handling Techniques gives great explanation and example for this problem. – antonio081014 Aug 23 '15 at 09:01
  • What does "top level" subview mean in this case? Does it go through the `subviews` array from index 0, or from the other end? – fatuhoku Aug 28 '15 at 17:49
  • It reverses the subview array and then sends in incrementing index order. i.e. logically equivalent to sending hittest: last index to fist index. – BangOperator Jul 29 '16 at 19:32
  • Ridiculously useful answer. I never would've guessed that `pointInside:withEvent` is **called by** `hitTest` on the same object. And this actually helped me resolve a massive issue today, thank you! – Dan Rosenstark Jan 09 '17 at 15:16
51

I find this Hit-Testing in iOS to be very helpful

enter image description here

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

Edit Swift 4:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    if self.point(inside: point, with: event) {
        return super.hitTest(point, with: event)
    }
    guard isUserInteractionEnabled, !isHidden, alpha > 0 else {
        return nil
    }

    for subview in subviews.reversed() {
        let convertedPoint = subview.convert(point, from: self)
        if let hitView = subview.hitTest(convertedPoint, with: event) {
            return hitView
        }
    }
    return nil
}
Samps
  • 799
  • 6
  • 12
onmyway133
  • 45,645
  • 31
  • 257
  • 263
  • So you need to add this to a subclass of UIView and have all the views in your hierarchy inherit from it? – Guig Jan 25 '19 at 02:07
21

Thanks for answers, they helped me to solve situation with "overlay" views.

+----------------------------+
|A +--------+                |
|  |B  +------------------+  |
|  |   |C            X    |  |
|  |   +------------------+  |
|  |        |                |
|  +--------+                | 
|                            |
+----------------------------+

Assume X - user's touch. pointInside:withEvent: on B returns NO, so hitTest:withEvent: returns A. I wrote category on UIView to handle issue when you need to receive touch on top most visible view.

- (UIView *)overlapHitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 1
    if (!self.userInteractionEnabled || [self isHidden] || self.alpha == 0)
        return nil;

    // 2
    UIView *hitView = self;
    if (![self pointInside:point withEvent:event]) {
        if (self.clipsToBounds) return nil;
        else hitView = nil;
    }

    // 3
    for (UIView *subview in [self.subviewsreverseObjectEnumerator]) {
        CGPoint insideSubview = [self convertPoint:point toView:subview];
        UIView *sview = [subview overlapHitTest:insideSubview withEvent:event];
        if (sview) return sview;
    }

    // 4
    return hitView;
}
  1. We should not send touch events for hidden or transparent views, or views with userInteractionEnabled set to NO;
  2. If touch is inside self, self will be considered as potential result.
  3. Check recursively all subviews for hit. If any, return it.
  4. Else return self or nil depending on result from step 2.

Note, [self.subviewsreverseObjectEnumerator] needed to follow view hierarchy from top most to bottom. And check for clipsToBounds to ensure not to test masked subviews.

Usage:

  1. Import category in your subclassed view.
  2. Replace hitTest:withEvent: with this
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

Official Apple's Guide provides some good illustrations too.

Hope this helps somebody.

Lion
  • 1,264
  • 1
  • 17
  • 23
4

It shows like this snippet!

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01)
    {
        return nil;
    }

    if (![self pointInside:point withEvent:event])
    {
        return nil;
    }

    __block UIView *hitView = self;

    [self.subViews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {   

        CGPoint thePoint = [self convertPoint:point toView:obj];

        UIView *theSubHitView = [obj hitTest:thePoint withEvent:event];

        if (theSubHitView != nil)
        {
            hitView = theSubHitView;

            *stop = YES;
        }

    }];

    return hitView;
}
hippo
  • 87
  • 4
  • I find this the easiest to understand answer, and it matches my observations of the actual behaviour very closely. The only difference is that the subviews are enumerated in reverse order, so subviews closer to the front receive touches in preference to siblings behind them. – Douglas Hill Jun 10 '15 at 12:26
  • @DouglasHill thanks to your correction. Best regards – hippo Jun 18 '15 at 07:58
4

iOS touch

1. User touch
2. event is created
3. hit testing by coordinates - find first responder - UIView and successors (UIWindow) 
 3.1 hit testing - recursive find the most deep view(the smallest)
  3.1.1 point inside - check coordinates
4. Send Touch Event to the First Responder

Class diagram

3 Hit Testing

Find a First Responder

First Responder in this case is the deepest(the smallest) UIView, point() method(hitTest() uses point() internally) of which returned true. It always go through UIApplication -> UIWindow -> First Responder

func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
func point(inside point: CGPoint, with event: UIEvent?) -> Bool

Internally hitTest() looks like

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
}

4 Send Touch Event to the First Responder

//UIApplication.shared.sendEvent()

//UIApplication, UIWindow
func sendEvent(_ event: UIEvent)

//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?)

Let's take a look at example

Responder Chain

It a kind of chain of responsibility pattern. It consists of UIResponser who can handle UIEvent. In this case it starts from first responder who overrides touch.... super.touch... calls next link in responder chain

Responder chain is also used by addTarget or sendAction approaches like event bus

//UIApplication.shared.sendAction()
func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool

Take a look at example

class AppDelegate: UIResponder, UIApplicationDelegate {
    @objc
    func foo() {
        //this method is called using Responder Chain
        print("foo") //foo
    }
}

class ViewController: UIViewController {
    func send() {
        UIApplication.shared.sendAction(#selector(AppDelegate.foo), to: nil, from: view1, for: nil)
    }
}

*isExclusiveTouch is taken into account when handling multitouch

[Android onTouch]

yoAlex5
  • 29,217
  • 8
  • 193
  • 205
1

The snippet of @lion works like a charm. I ported it to swift 2.1 and used it as an extension to UIView. I'm posting it here in case somebody needs it.

extension UIView {
    func overlapHitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        // 1
        if !self.userInteractionEnabled || self.hidden || self.alpha == 0 {
            return nil
        }
        //2
        var hitView: UIView? = self
        if !self.pointInside(point, withEvent: event) {
            if self.clipsToBounds {
                return nil
            } else {
                hitView = nil
            }
        }
        //3
        for subview in self.subviews.reverse() {
            let insideSubview = self.convertPoint(point, toView: subview)
            if let sview = subview.overlapHitTest(insideSubview, withEvent: event) {
                return sview
            }
        }
        return hitView
    }
}

To use it, just override hitTest:point:withEvent in your uiview as follows:

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    let uiview = super.hitTest(point, withEvent: event)
    print("hittest",uiview)
    return overlapHitTest(point, withEvent: event)
}
mortadelo
  • 145
  • 1
  • 11