This is absolutely confusing. On a 3.5" iPhone simulator, all of the UIButtons on my app work just fine. However, when I launch on the 4" iPhone simulator, all of the UIButtons on the left side of the app do not receive any click events.
Below are screenshots of the 3.5" size and the 4" size. On the 4" size, I've added a line. Left of that line, none of the buttons receive click events. To the right of that line, all buttons behave normally. The left side of buttons 2, 5, and 8 do not respond to clicks, but the right sides of those buttons do respond.
UPDATE----
Thanks to @iccir, I've discovered more info. Apparently, my UIWindow is only 320x480 instead of 568x320 as it should be. I'm not touching the UIWindow in my code except to make it key and visible. In my MainWindow.xib I connect its IBOutlet to my rootViewController.
<UIWindow: 0xc097d50; frame = (0 0; 320 480); opaque = NO; autoresize = RM+BM; gestureRecognizers = <NSArray: 0xc098460>; layer = <UIWindowLayer: 0xc097e70>>
I'm flabberghasted. Any idea why the UIWindow is incorrectly sized?
This is a pretty common issue: Your UIButton is outside of the bounds of one of its superviews. If clipsToBounds
/masksToBounds
is set to NO (the default), your UIButton is still going to show up, but touch events aren't going to be sent to it.
Let's simplify this case. Suppose a view controller with the following code:
- (void) viewDidLoad
{
[super viewDidLoad];
UIColor *fadedRedColor = [UIColor colorWithRed:1 green:0 blue:0 alpha:0.25];
UIColor *fadedBlueColor = [UIColor colorWithRed:0 green:0 blue:1 alpha:0.25];
CGRect containerFrame = CGRectMake(25, 25, 100, 100);
CGRect buttonFrame = CGRectMake(100, 100, 64, 44);
UIView *container = [[UIView alloc] initWithFrame:containerFrame];
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
[button setTitle:@"Button" forState:UIControlStateNormal];
[button setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[button setFrame:buttonFrame];
[button addTarget:self action:@selector(_handleButton:) forControlEvents:UIControlEventTouchUpInside];
[container setBackgroundColor:fadedRedColor];
[button setBackgroundColor:fadedBlueColor];
[container addSubview:button];
[[self view] addSubview:container];
}
- (void) _handleButton:(id)sender
{
NSLog(@"Moooooo!");
}
Which looks like this:
The button is contained in container
, but it resides outside of the container's bounds (the container is 100 pixels wide and 100 pixels tall, the button's origin is at 100, 100
).
When you touch the screen, UIKit is going to start at the top of the view hierarchy (UIWindow) and call -[UIView hitTest:withEvent:]
recursively until it finds the view that should handle the touch. However, in this example, UIKit will never descend into the container (since you touched outside its boundary), and thus the button subview will not be hit.
If we instead change the buttonFrame
to be 50, 50
, it looks like this:
The part of the button that overlaps with the container will respond to touch event. The part that resides outside of the container will not:
To debug a view that isn't fully touchable, you can try a debugging function like the following:
static void sDebugViewThatIsntTouchable(UIView *view)
{
UIView *superview = [view superview];
while (superview) {
CGRect rectInSuperview = [view convertRect:[view bounds] toView:superview];
CGPoint topLeft = CGPointMake(CGRectGetMinX(rectInSuperview), CGRectGetMinY(rectInSuperview));
CGPoint topRight = CGPointMake(CGRectGetMaxX(rectInSuperview), CGRectGetMinY(rectInSuperview));
CGPoint bottomLeft = CGPointMake(CGRectGetMinX(rectInSuperview), CGRectGetMaxY(rectInSuperview));
CGPoint bottomRight = CGPointMake(CGRectGetMaxX(rectInSuperview), CGRectGetMaxY(rectInSuperview));
if (![superview pointInside:topLeft withEvent:nil]) {
NSLog(@"Top left point of view %@ not inside superview %@", view, superview);
}
if (![superview pointInside:topRight withEvent:nil]) {
NSLog(@"Top right point of view %@ not inside superview %@", view, superview);
}
if (![superview pointInside:bottomLeft withEvent:nil]) {
NSLog(@"Bottom left point of view %@ not inside superview %@", view, superview);
}
if (![superview pointInside:bottomRight withEvent:nil]) {
NSLog(@"Bottom right point of view %@ not inside superview %@", view, superview);
}
superview = [superview superview];
}
};
Edit:
As you mentioned in the comments, the culprit view was the main UIWindow, which was sized to 320x480 rather than 320x568. Turning on "Full Screen at Launch" in the xib fixed this.
Of course, the question is: "Why?" :)
If you pull up your xib file in a text editor, you will notice that a width of 320 and height of 480 are hardcoded to the window. When the xib is decoded at launch time, the window is initially constructed with this 320x480 frame.
UIKit then queries -[UIWindow resizesToFullScreen]
(a private method). If this returns YES, the UIWindow does the equivalent of [self setFrame:[[self window] bounds]]
.
Toggling the "Full Screen at Launch" flag in Interface Builder directly toggles the private UIWindow.resizesToFullScreen
flag.
Let me guess, this happens only in the landscape mode, right ?
I had the same issue in my app when I was developing specifically for the iPhone-4S. But when I began testing on the iPhone-5, touches on the bottom did not work. It were the frame
. Make sure frames
are set to bounds
, both in the code, and the XIB
file (if there is one). Different frames
/bounds
in the man
and xib
files, might also result in such behaviour.
I eventually removed the xib
files, and did everything programmatically. One thing I learnt was, set your frames in viewWillAppear
or viewDidAppear
methods instead of viewDidLoad
. Also check the frame
/bounds
in your RootViewController
. One last thing, try not to use constant value for frames
, use referential frames with respect to superview.
PS, one way to know if it really is the frames/bounds that are responsible for this behaviour is, setting the masksToBounds
to YES
, for the views
. That way, your views will not be visible outside their rects.