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.
Related
so I have a very simple button that when clicked goes to fullscreen and when clicked again goes back to the same position it was initially in. For some reason it works perfectly without the animation. When I uncomment the animation part when I initially click the button it does nothing, the second time I click it slightly enlarges. The third time I click it animates slowly but back to it's smaller original size... Why is it animating the opposite way?
- (IBAction)viewImage1:(id)sender {
UIButton *btn = (UIButton*) sender;
if (btn.tag == 0)
{
CGRect r = [[UIScreen mainScreen] bounds];
/*[UIView animateWithDuration:0.5f delay:0.0f options:0 animations:^{*/
[sender setFrame: r];
/*}completion:nil];*/
btn.tag = 1;
}
else
{
btn.tag = 0;
[sender setFrame:CGRectMake(0,0,370,200)];
}
}
There are two solutions to your problem either of which will work:
Disable Autolayout. (discouraged)
You can do that in Interface Builder by opening the File Inspector
in the right pane and unchecking the respective check box.
However, if you want to use Autolayout for constraining other UI elements in your view (which is quite a good idea in most cases) this approach won't work which is why I would recommend the second solution:
Keep Autolayout enabled, create an outlet for your button in your view controller and set
self.myButton.translatesAutoresizingMaskIntoConstraints = YES;
in your view controller's viewDidLoad method.
You could also add layout constraints to your button and animate those. (This excellent Stackoverflow post explains how it's done.)
The reason for this tricky behavior is that once you enable Autolayout a view's frame is no longer relevant to the actual layout that appears on screen, only the view's layout constraints matter. Setting its translatesAutoresizingMaskIntoConstraints property to YES causes the system to automatically create layout constraints for your view that will "emulate" the frame you set, in a manner of speaking.
It is easier to do this with auto layout and constraints. Create an IBOutlet for the height constraint of your button call it something like btnHeight. Do the same for the width constraint call it something like btnWidth. Then create an IBAction like so:
- (IBAction)buttonPress:(UIButton *)sender {
UIButton *btn = (UIButton *) sender;
CGRect r = [[UIScreen mainScreen] bounds];
if (CGRectEqualToRect(btn.frame, CGRectMake(0.0, 0.0, 370, 200))) {
[UIView animateWithDuration:0.5 delay:0.0 options:0 animations:^{
self.btnHeight.constant = r.size.height;
self.btnWidth.constant = r.size.width;
[self.view layoutIfNeeded];
} completion:^(BOOL finished){
}];
}else{
[UIView animateWithDuration:0.5 delay:0.0 options:0 animations:^{
self.btnHeight.constant = 200.0;
self.btnWidth.constant = 370.0;
[self.view layoutIfNeeded];
} completion:^(BOOL finished){
}];
}
}
In my experience animating the frame of a UIButton does not work well, a, the only method, I'm aware of, is to use CGAffineTransformScale which will rasterize the title of the button and scale it as well.
I have a reusable view I will be using in UITableViewCell's and UICollectionViewCell's, and need to get its dimensions for tableView:heightForRowAtIndexPath:. Some subviews have stuff going on inside layoutSubviews so I can't call systemLayoutForContentSize:, instead my plan is to:
Instantiate the metrics view.
Set the size to include the desired width.
Populate it with data.
Update constraints / Layout subviews.
Grab the height of the view or an internal "sizing" view.
The problem I'm running into is that I cannot force the view to layout without inserting it into the view and waiting for the runloop.
I've distilled a rather boring example. Here's View.xib. The subview is misaligned to highlight that the view is never getting laid out even to the baseline position:
On the main thread I call:
UIView *view = [[UINib nibWithNibName:#"View" bundle:nil] instantiateWithOwner:nil options:nil][0];
NSLog(#"Subviews: %#", view.subviews);
view.frame = CGRectMake(0, 0, 200, 200);
[view updateConstraints];
[view layoutSubviews];
NSLog(#"Subviews: %#", view.subviews);
[self.view addSubview:view];
[view updateConstraints];
[view layoutSubviews];
NSLog(#"Subviews: %#", view.subviews);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Subviews: %#", view.subviews);
});
I get out the following view information:
1) "<UIView: 0x8bad9e0; frame = (50 50; 220 468); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
2) "<UIView: 0x8bad9e0; frame = (50 50; 220 468); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
3) "<UIView: 0x8bad9e0; frame = (50 50; 220 468); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
4) "<UIView: 0x8bad9e0; frame = (0 100; 100 100); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
1 indicates that the fresh-out-of-the-NIB view hasn't been laid out. 2 indicates that updateConstraints/layoutSubviews did nothing. 3 indicates that adding it to the view hierarchy did nothing. 4 finally indicates that adding to the view hierarchy and one pass through the main-loop laid out the view.
I would like to get to the point where I can get the view's dimensions without having to let the application handle it or perform manual calculations (string height + constraint1 + constraint2) on my own.
Update
I've observed that if I place view inside a UIWindow I get a slight improvement:
UIView *view = [[UINib nibWithNibName:#"View" bundle:nil] instantiateWithOwner:nil options:nil][0];
UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
view.frame = CGRectMake(0, 0, 200, 200);
[window addSubview:view];
[view layoutSubviews];
If view.translatesAutoresizingMaskIntoConstraints == YES, the view's immediate subviews will be laid out, but none of their children.
The Autolayout Question
In the basic case you mentioned, you can get the correct size by calling setNeedsLayout and then layoutIfNeeded on the container view.
From the UIView class reference on layoutIfNeeded:
Use this method to force the layout of subviews before drawing. Starting with the receiver, this method traverses upward through the view hierarchy as long as superviews require layout. Then it lays out the entire tree beneath that ancestor. Therefore, calling this method can potentially force the layout of your entire view hierarchy. The UIView implementation of this calls the equivalent CALayer method and so has the same behavior as CALayer.
I don't think the "entire view hierarchy" applies to your use case since the metrics view presumably wouldn't have a superview.
Sample Code
In a sample empty project, with just this code, the correct frame is determined after layoutIfNeeded is called:
#import "ViewController.h"
#interface ViewController ()
#property (nonatomic, strong) UIView *redView;
#end
#implementation ViewController
#synthesize redView;
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
redView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 220, 468)];
redView.backgroundColor = [UIColor redColor];
redView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:redView];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{50, 50}, {220, 468}}"
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[redView(==100)]" options:0 metrics:Nil views:NSDictionaryOfVariableBindings(redView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-100-[redView(==100)]" options:0 metrics:Nil views:NSDictionaryOfVariableBindings(redView)]];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{50, 50}, {220, 468}}"
[self.view setNeedsLayout];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{50, 50}, {220, 468}}"
[self.view layoutIfNeeded];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{0, 100}, {100, 100}}"
}
#end
Additional Considerations
Slightly outside the scope of your question, here are some other issues you may run into, since I've worked on this exact problem in a real app:
Calculating this in heightForRowAtIndexPath: might be expensive, so you may want to precalculate and cache the results
Precalculation should be done on a background thread, but UIView layout doesn't work well unless it's done on the main thread
You should definitely implement estimatedHeightForRowAtIndexPath: to reduce the impact of these performance issues
Using intrinsicContentSize
In response to:
Some subviews have stuff going on inside layoutSubviews so I can't call systemLayoutForContentSize:
You can use this method if you implement intrinsicContentSize, which lets a view suggest an optimal size for itself. One implementation for this might be:
- (CGSize) intrinsicContentSize {
[self layoutSubviews];
return CGSizeMake(CGRectGetMaxX(self.bottomRightSubview.frame), CGRectGetMaxY(self.bottomRightSubview.frame));
}
This simple approach will only work if your layoutSubviews method doesn't refer to an already-set size (like self.bounds or self.frame). If it does, you may need to do something like:
- (CGSize) intrinsicContentSize {
self.frame = CGRectMake(0, 0, 10000, 10000);
while ([self viewIsWayTooLarge] == YES) {
self.frame = CGRectInset(self.frame, 100, 100);
[self layoutSubviews];
}
return CGSizeMake(CGRectGetMaxX(self.bottomRightSubview.frame), CGRectGetMaxY(self.bottomRightSubview.frame));
}
Obviously, you'd need to adjust these values to match the particular layout of each view, and you may need to tune for performance.
Finally, I'll add that due in part to the exponentially increasing cost of using auto-layout, for all but the simplest table cells, I usually wind up using manual height calculation.
Presumably you're calling the demo code when the view controller first loads its view, like in viewDidLoad or another life cycle method. The nested subview's geometries won't reflect its constraints until viewDidLayoutSubviews is called. Nothing you do during the initial life cycle of a view controller will make that method arrive any faster.
Update 12/30/13: After testing Aaron Brager's sample code, I now realize that the previous paragraph is incorrect. Apparently, you can force layout in viewDidLoad by calling setNeedsLayout followed by layoutIfNeeded.
If you executed the demo code in response to a button click instead, I think you'll see the final geometries of your nested subview logged before the action method completes.
- (IBAction)buttonTapped:(id)sender
{
UIView *view = [[UINib nibWithNibName:#"View" bundle:nil] instantiateWithOwner:nil options:nil][0];
view.frame = CGRectMake(0, 0, 200, 200);
[self.view addSubview:view];
[self.view layoutIfNeeded];
NSLog(#"Subviews: %#", view.subviews);
}
In the latter case, you can request layout on-demand because the view controller has completed its initial setup.
But during a view controller's initial setup, how are you going to get the final geometries of your re-usable subview?
After you set the content for the re-usable subview, have your view controller ask the subview for its size. In other words, implement a method on your custom view that calculates the size based on the content.
For example, if the subview's content is an attributed string, you could use a method like boundingRectWithSize:options:context: to help determine the size of your subview.
CGRect rect = [attributedString boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX) options:NSStringDrawingUsersLineFragmentOrigin context:nil];
My universal app uses NIBs for its settings screens. I'd like to use the same NIBs for both iPhone and iPad.
Thus on iPad, I use a UIPopoverController in the MainViewController and for settings, simply display the iPhone-sized NIBs, to show what is called the SettingsViewController. The popover is sized 320x460 points.
This causes a problem, because the iPhone version draws a number of things above the status bar programmatically, and for the iPad version this is not necessary. Current situation on iPad:
As you can see, there's a big empty space above the "Settings" title. Thus what I want, is to shift the view controller up about 20 points, inside the popover:
The popover is instantiated as follows in the MainViewController:
settingsPopoverController = [[UIPopoverController alloc] initWithContentViewController:popoverNavigationController];
settingsPopoverController.popoverContentSize = CGSizeMake(320, 460);
settingsPopoverController.delegate = self;
popoverNavigationController.navigationBarHidden = YES;
In the SettingsViewController, I set the frame as follows:
- (void)viewDidLoad
{
self.contentSizeForViewInPopover = CGSizeMake(320, 460);
}
And later in the SettingsViewController, I try to create an offset as follows:
- (void)viewWillLayoutSubviews
{
// shift it up
CGRect frame = self.view.frame;
frame.origin.y = -20;
[self.view setFrame:frame];
}
This does not shift the content up a bit. How to go about?
To clarify: I want to move down the "viewport" that the popover shows.
Try to:
myPopover.autoresizingMask=UIViewAutoresizingNone;
Or:
myPopover.autoresizingMask=UIViewAutoresizingFlexibleHeight || UIViewAutoresizingFlexibleWidth;
You can also try to put your code in -(void)viewDidLayoutSubviews method.
If this answers did not help, try set popoverLayoutMargins (property) instead of setFrame: for example:
popover.popoverLayoutMargins=UIEdgeInsetsMake (
CGFloat 50, //top
CGFloat 50,//left
CGFloat 50,//bottom
CGFloat 50//right
);
Im trying to implement my own CustomUIActionSheet.
I have it almost working, but I have no idea how does the showInView method works.
(void)showInView:(UIView *)view
giving a view, this method is capable of put its view in front of every single view (adding it to the windows maybe?) but its also capable of settings the rotation accordingly to the view in which is being added.
Ive tried adding it to the windows of the view that I recieve as a parameter.
CGFloat startPosition = view.window.bounds.origin.y + view.window.bounds.size.height;
self.frame = CGRectMake(0, startPosition, view.window.bounds.size.width, [self calculateSheetHeight]);
[view.window addSubview:self];
self.blackOutView = [self buildBlackOutViewWithFrame:view.window.bounds];
[view.window insertSubview:self.blackOutView belowSubview:self];
By doing this, all works, except that when I present the action sheet in landscape, the action sheet apears from the right (since the windows system reference its always the same)
I´ve also tried to add it to the rootViewController of the view windows like that:
UIView * view = view.window.rootViewController.view;
CGFloat startPosition = view.bounds.origin.y + view.bounds.size.height;
self.frame = CGRectMake(0, startPosition, view.bounds.size.width, [self calculateSheetHeight]);
[view addSubview:self];
self.blackOutView = [self buildBlackOutViewWithFrame:view.bounds];
[view insertSubview:self.blackOutView belowSubview:self];
but again, it fails in landscape, it doesnt add anything or at least, I can not see it
So, my question is, any clue of how can I add a view to the top of the hierarchy working in both orientations?
thanks a lot!
After reading some source code of custom alert views and other ui components that are dranw in the top of the hierarchy I found a working solution.
Adding the view in the first subview of the windows:
UIView * topView = [[view.window subviews] objectAtIndex:0];
So the final code is
UIView * topView = [[view.window subviews] objectAtIndex:0];
CGFloat startPosition = topView.bounds.origin.y + topView.bounds.size.height;
self.frame = CGRectMake(0, startPosition, topView.bounds.size.width, [self calculateSheetHeight]);
[topView addSubview:self];
self.blackOutView = [self buildBlackOutViewWithFrame:topView.bounds];
[topView insertSubview:self.blackOutView belowSubview:self];
I have a UITableView with a custom header (i.e. I create the UIView myself). I need to tweak the accessibilityFrame of one of the subviews of the view, but I can’t figure out how to set the coordinates of the frame appropriately—they need to be relative to the window, but I’m not sure how to accomplish that.
My code looks like
- (UIView *)tableView:(UITableView *)tableView
viewForHeaderInSection:(NSInteger) section
{
CGRect bounds = CGRectMake(0, 0, [tableView frame].size.width, 48);
UIView *header = [[UIView alloc] initWithFrame:bounds];
UILabel *labelOne = [[UILabel alloc] initWithFrame:
CGRectMake(0, 0, bounds.size.width - 80, 18)];
UILabel *labelTwo = [[UILabel alloc] initWithFrame:
CGRectMake(0, 20, bounds.size.width - 80, 18)];
CGRect frameOne = [labelOne frame];
CGRect frameTwo = [labelTwo frame];
[labelTwo setIsAccessibilityElement:NO];
[labelOne setAccessibilityFrame:CGRectUnion(frameOne, frameTwo)];
// ...
return header;
}
I’ve got two UILabels, which I want to combine into one for the purposes of VoiceOver. I accomplish this by ignoring the second label and extending the frame of the first label to cover the area of the second label. (The second label is immediately below the first.) The problem is getting the frames. If I use the code as shown above, the accessibility frame is the correct size, but is positioned as if the UITableView’s header were in the top left corner of the screen. I tried to modify the code to say
CGRect frameOne = [header convertRect:[labelOne frame] toView:nil];
CGRect frameTwo = [header convertRect:[labelTwo frame] toView:nil];
but the same thing happened. Shouldn’t this latter piece of code convert the UILabels’ frames into window-relative coordinates?
I thought maybe the issue is that when the UIView is created, it doesn’t know where on screen it’s going to be positioned (and as part of a UITableView it may be scrolled all over the place). Is it necessary to implement accessibilityFrame as a message which checks the UIView’s position each time it is called?
There's a helper function that will assist you with doing exactly that: UIAccessibilityConvertFrameToScreenCoordinates. This function takes a CGRect and converts it from a view's coordinate system into screen coordinates.
I don't think it's the timing of when the UIView is created, as I believe the window should be not-nil by the time tableView:viewForHeaderInSection: is called. I think the problem is the receiver of the convertRect:toView: message. Rather than passing this message to header, you should be passing it to [self view].
You're converting from the receivers coordinate system to that of another view, in this case nil or the UIWindow in your app. When header receives this message, you're converting from header's coordinate system to window's coordinate system, but header itself is a subview of [self view]. Instead, you want to ask [self view] to do the conversion, which should take into account any UINavigationBar's, etc.
CGRect frameOne = [[self view] convertRect:[labelOne frame] toView:nil];
CGRect frameTwo = [[self view] convertRect:[labelTwo frame] toView:nil];