49

I can't figure out why tapping on text fields and buttons in my view is not working. I've checked all the obvious things like whether userInteractionEnabled is set to YES, whether a gesture recognizer is installed, and if there is an invisible view in the foreground.

Is there a best practice in iOS for tracing a touch from when it first appears to where it gets consumed?

UPDATE:

Both answers were helpful. During the course of my investigation I learned that if a subview is outside of its parent's bounds, even if the parent is not clipping subviews, the subview will not receive events. I printed out the chain of superviews from the text field that was not getting touches, and I saw that one of those views had a height of 0. I put in some constraints to stretch it out, and my problem was solved.

bugloaf
  • 2,890
  • 3
  • 30
  • 49
  • 1
    The zero-size view bit me. I had made some intermediate views to keep z-order sane, but I forgot to update the frames of the intermediate views, so they stayed at zero size. Once I updated them in `LayoutSubviews` taps started flowing again. – Anthony Mills Jul 19 '16 at 19:58
  • I was just bitten by this too. In summary, check two things in the hierarchy of the view which has this problem (assuming it's not covered by a non-interactive transparent view) : is userInteractionEnabled == true, and if height and width are > 0. – Guillaume Laurent Jun 16 '17 at 15:34

7 Answers7

58

You can debug that by using a symbolic breakpoint:

-[UIWindow sendEvent:] & po $arg3

enter image description here Logs:

<UITouchesEvent: 0x6000026fa6d0> timestamp: 179462 touches: {(
<UITouch: 0x7f84d6f10380> phase: Began tap count: 1 force: 0.000 
window: <UIWindow: 0x7f84d6d0ad10; frame = (0 0; 375 812); autoresize = W+H; 
gestureRecognizers = <NSArray: 0x600001aa8870>; layer = <UIWindowLayer: 0x6000014bd7e0>> view: <UIView: 0x7f84d6d0bff0; frame = (0 0; 375 812); 
autoresize = W+H; layer = <CALayer: 0x6000014bdc60>> location in window: {165.66665649414062, 232.33332824707031} previous location in window: 
{165.66665649414062, 232.33332824707031} location in view: {165.66665649414062, 
232.33332824707031} previous location in view: {165.66665649414062,232.33332824707031}
)}
HamzaGhazouani
  • 6,464
  • 6
  • 33
  • 40
  • 3
    Great answer. I also recommend checking the "Automatically continue after evaluating actions" if you just want the logs and not the breakpoint stopping the debugger – joehinkle11 Oct 26 '20 at 18:43
25

You can subclass UIWindow and override -[UIWindow sendEvent]:, then when it is called, use -[UIWindow hitTest:withEvent:] to test which view will receive the event.

You can then call -[UIView recursiveDescription] to print some debug information help you understand why that view received the touch event.

Remember to call [super sendEvent:] when you are done.


For those who use Storyboard and want to know how can we change the main application window class:

class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow? = TestWindow()
    // ....
}

Just provide the default value for AppDelegate's window var. When the app is launched the UIApplicationMain will instantiate a delegate and ask it for the window. If the window is nil it will create a new one automatically. But if we provide a default value here, it will be used all over the app.

kelin
  • 11,323
  • 6
  • 67
  • 104
Bryan Chen
  • 45,816
  • 18
  • 112
  • 143
  • what value should we set for the first parameter of hitTest? @Bryan – Yuwen Yan Mar 06 '19 at 00:40
  • A CGPoint, which can be read from the event. you need to check if it is a touch event first – Bryan Chen Mar 06 '19 at 04:04
  • If anyone would need to implement it, due to the fact it uses private API, here's the full code for it in objc: `- (void)sendEvent:(UIEvent *)event { UITouch *touch = [event.allTouches anyObject]; UIView *view = [self hitTest:[touch locationInView:touch.view] withEvent:event]; NSLog(@"Touch captured: %@", [view valueForKey:@"recursiveDescription"]); [super sendEvent:event]; }` – Nat May 30 '19 at 08:27
17

Not a direct answer to the question, but a very common cause of vanishing touches is for a control to be in a uiview which has a smaller size than that control, but is not clipping its bounds, so you won't see that the parent view is smaller (or even possibly zero-sized).

Parent UIView size=0x0, clipToBounds=false

   Child UIButton size=100x50

=> Child Button won't get any touch events.

Guillaume Laurent
  • 1,734
  • 14
  • 11
4

You can use the new Xcode6 live view debugging and by turning the view hierarchy in 3D you can see which views are above the ones you care for and inspect them.

DeFrenZ
  • 2,172
  • 1
  • 20
  • 19
3

My full Swift Example following @Bryan Chen and @kelin Answer ...

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow? = TestWindow()
    // ....
}

...

class TestWindow:UIWindow {
    override func sendEvent(_ event: UIEvent) {

        print(event)

        //<UITouchesEvent: 0x6000008b1f80> timestamp: 366545 touches: {(
        //    <UITouch: 0x7fa056d37dc0> phase: Ended tap count: 1 force: 0.000 
        //window: <zollundpost.TestWindow: 0x7fa056c020a0; baseClass = UIWindow; 
        //frame = (0 0; 375 812); gestureRecognizers = <NSArray: 0x6000034f9530>; 
        //layer = <UIWindowLayer: 0x600003abd940>> view: <UITableViewCellContentView: 
        //0x7fa059a55090; frame = (0 0; 375 72); opaque = NO; gestureRecognizers = 
        //<NSArray: 0x6000034293e0>; layer = <CALayer: 0x600003ac8320>> location in 
        //window: {165, 358.33332824707031} previous location in window: 
        //{144.33332824707031, 358.66665649414062} location in view: {165, 
        //44.999992370605469} previous location in view: {144.33332824707031, 
        //45.333320617675781}
        //    )}

        super.sendEvent(event)
    }
}
Peter Kreinz
  • 7,979
  • 1
  • 64
  • 49
2

To see more detail about which kind of gesture recognizers is source of issue from @Peter's answer, I print out the array of gestureRecognizer:

class TestWindow:UIWindow {
    override func sendEvent(_ event: UIEvent) {

        event.allTouches?.forEach() { touch in
            print("touch")
            print(touch.gestureRecognizers ?? "")
            /// [<UILongPressGestureRecognizer: 0x7fb205964ab0; state = Possible; view = <UIView 0x7fb205964940>; target= <(action=_handleMenuGesture: ....
        }

        super.sendEvent(event)
    }
}

Bill Chan
  • 3,199
  • 36
  • 32
0

A basic way to see what is consuming the touch events is to simply add a debug view and override the hitTest(:event:) method. Attach this class to any to the view in the storyboard.

class DebugView: UIView {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let view = super.hitTest(point, with: event)
        print("View")
        print(view)
        return view
    }
}

Attaching this to the root view of the controller will give you access to the view that is consuming the touch event. Note that the hitTest method will be called twice with every touch event.