Unresponsive UI

Here are some tips for debugging unresponsive UI elements, such as a UIButton or UISlider. These tips work for me at least 90% of the time I have this problem, which is great because there are many possible causes.

Use recursiveDescription

The single most useful advice here is to use the undocumented API call [UIView recursiveDescription], which returns an NSString describing, in a mostly human-friendly way, the entire view hierarchy rooted at the receiver. I commonly use this method from my app delegate, to get a very comprehensive snapshot of how my view is working shortly after app startup:

- (void)printViewHierarchy {
  // 1. Ignore the warning; 2. Remove before submitting to the app store.
  NSLog(@"%@", [self.window recursiveDescription]);
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
  MainViewController *mainController = [MainViewController new];
  [self.window addSubview:mainController.view];
  [self.window makeKeyAndVisible];
 
  [self performSelector:@selector(printViewHierarchy)
             withObject:nil
             afterDelay:2.0 /* enough time for view to be set up */ ];
   
  return YES;
}

In a sample app I wrote, the output from recursiveDescription looks like this:

2011-02-13 23:03:37.882 Temp23[11235:207] <UIWindow: 0x4b43c30; frame = (0 0; 320 480); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x4b43ce0>>
   | <UIView: 0x4b436b0; frame = (0 20; 320 460); layer = <CALayer: 0x4b436e0>>
   |    | <UISwitch: 0x4b45a10; frame = (110 100; 94 27); clipsToBounds = YES; layer = <CALayer: 0x4b45b00>>
   |    |    | <UIView: 0x4b49c70; frame = (91 0; 3 27); layer = <CALayer: 0x4b49830>>
   |    |    | <UIView: 0x4b438a0; frame = (0 0; 0 27); layer = <CALayer: 0x4b129b0>>

We can easily see at a glance all the views on the screen their relative sizes and positions, and any non-default properties, such as “opaque=NO” on the root UIWindow.

Here are things that might cause your view to be unresponsive to touches:

  1. Check that your view’s frame is accessible. This requires that your view’s frame, and all of it’s ancestor frames, are nonzero in size, and contained within each other. This also requires that no non-descendants are in front of your view. It’s ok if your view does have some descendants (subviews or subviews-of-subviews, etc), as long as those elements don’t block UI. For example, UIImageViews don’t block UI to their superviews.
  2. Check for settings that block user interaction. If your view or any ancestor of your view has userInteractionEnabled=NO, your view won’t get touch events. If your view or any ancestor has hidden=YES, your view won’t get touch events. Same thing for alpha < 0.1. Any of these “blocking” values will show up in the output of recursiveDescription.
  3. Finally, make sure to verify you’re calling recursiveDescription at the right time. One common mistake is to call this method before the view is done setting up, such as from the init method of a view controller that loads from a nib file. In that case, you’d want to wait until viewDidLoad was called. I often use the performSelector:withObject:afterDelay: method to be on the safe side here.

You’ll get a compiler warning for this code, because the recursiveDescription method is not declared in any header file you have access to. However, the method does exist, and will be called correctly. If you want to get rid of the warning, you can declare the interface of a category on UIView that includes this method. You also have to remove all calls to recursiveDescription before submitting to Apple, since this is an undocumented API.

Checking connections

If you’ve already checked the output of recursiveDescription, I have two more suggestions to help narrow down the problem:

  1. Add a UIButton as a subview of your view to verify that touches are getting to your view. For example, something like this:
      // aSwitch is the unresponsive view.
      UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
      [button addTarget:self action:@selector(buttonTapped)
       forControlEvents:UIControlEventTouchUpInside];
      button.frame = aSwitch.bounds;
      [aSwitch addSubview:button];

    If the button fails, this means touches aren’t getting to your view in the first place. Try looking again at recursiveDescription. If your view is in a UIScrollView, including as part of a UITableView, then make sure you understand how UIScrollView kind of hijacks some touch events, since it’s always listening for scrolls.

  2. If your view is a UIControl, simulate a fake touch on your view to verify that your receiving methods are listening. Something like this:
      [aSwitch sendActionsForControlEvents:UIControlEventValueChanged];

    If this does not call your listening method, then something has gone wrong with adding your target/action to the UIControl. More times than I care to admit, I’ve realized at this point that I simply forgot to call [UIControl addTarget:action:forControlEvents:] ! How embarrassing.

Hope that helps!

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*