How come some of my UIViews are shifted after navigation? - uiview

In some of my application designs or for just some UIViews, following a navigationController's pushViewController, my new view will be shifted off the window by the height of the status bar. As a result, I will put this code stub in the viewDidLoad method.
CGRect frameAt = [self.view frame];
CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame];
frameAt.origin.y += statusBarFrame.size.height;
[self.view setFrame: frameAt];
It does not make sense to me that this is the intention of XCode and Interface Builder, so I suspect that I am doing something fundamentally wrong with the SDK during my view design. Furthermore, on the rare occasion that I don't have to shift my view, I really don't know what the difference in the two design approaches.
Note also, that most of the time I try to design my views using IB, with some minor customization.
Does anyone else run into this and know what they do to fix without such a code stub?

I've used Apple's NavBar sample code to try and reproduce this problem.
the applicationDidFinishLaunching is originally implemented like this:
[window addSubview:navigationController.view];
[window makeKeyAndVisible];
If I change it to this:
UIViewController *shellController = [[UIViewController alloc] initWithNibName:nil bundle:nil];
[shellController.view addSubview:navigationController.view];
[window addSubview:shellController.view];
[window makeKeyAndVisible];
Then I get the gap appearing.
However if I only do this:
UIView *shell = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
[shell addSubview:navigationController.view];
[window addSubview:shell];
[window makeKeyAndVisible];
Then everything looks normal.
So I guess every view controller will offset its view if its the root view controller. But the navigation controller overrides this behaviour to offset its view regardless of whether it's the root view controller. Therefore I would say your gap is appearing because you've got a navigation controller somewhere lower in the hierarchy than it's supposed to be.

The main thing to keep in mind here is that a view controller will set the frame of its view itself. This is because the amount of space available to the view may change during the lifetime of the app, and only the view controller knows how to adjust the view's frame appropriately. Examples of when the amount of space changes include the navigation bar changing height, turning the device from portrait to landscape and the status bar can also increase in height if the user is taking a call. Because of this you should not modify the view's frame yourself.
So, the first thing you need to is is remove all code related to modifiying the view's frame.
Now you need to design your views with the mindset that the frame size could change at any moment. This means setting the autoresizing property of each subview properly. If you do this, then it won't matter if you turn on the simulated navigation and status bars or not; they're just there to help you see what the final result will look like in most cases.
You can set the autoresizing property of each subview in Interface Builder in the Size Inspector (the one with the ruler icon). In the animation, the white box represents the root view of the view controller, the red box represents the currently selected subview. You'll notice that the subview is anchored to the top-left corner of the root view by default. This is fine if the size of the view never changes, but we know that not to be true. If you have subviews that you want to appear at the bottom no-matter-what, then you need to play with the diagram to the left. The way it works is if one of the four lines around the edge is selected, then the distance between that edge of the root view and the edge of the subview is fixed. So if you want a subview to appear at the bottom, you need to make sure the bottom-most line is selected and not the top. The two lines in the middle affect whether the size of the subview changes when the root view change size. So, for example, if you had a table view that you wanted to occupy the entire height of the screen, you would make sure the inner vertical line was selected. This is called the struts and springs model.
If you are adding subviews programatically you need to set the autoresizingMask property on each subview. Here's an explanation.
Hope that helps!

I've run into similar issues. Check out my two previous questions:
IPhone - After dismissing Modal View Controller - gap is left at top of page
IPhone - UIView addSubview Gap at top

link text
A similar bug is discussed here.
Also is animation set to NO? Try setting it to YES as this solved a similar problem I was facing.

Moshy's answer was very helpful as I finally realized the meaning of the dotted/solid lines in IB for controlling the resize properties of UIView elements.
However, adjusting those properties did not address a similar problem I faced with one of my views. This view had a status and top bar defined in IB. It was a slightly heavy one,
containing a UIWebView that would load a HTML string within viewWillAppear and a few other interface elements.
While loading the view, if the user suddenly changed the orientation of the device from portrait to landscape, all contents of the view would shift downwards by the height of the status bar. The resulting gap between the view controls and its top would remain even after switching back to portrait orientation.
What finally solved my issues, and my remaining hair, was adding the line:
self.view.frame = [[UIScreen mainScreen] bounds];
within
-(void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
Ever since my view contents are held in place despite abrupt device orientation changes.

Related

Applying bar metrics on a standalone UINavigationBar

My app has a view controller that due to the fact it plays its own custom transition animations, provides its own standalone UINavigationBar view at the top (As opposed to using a UINavigationController).
When using an iPhone, and when rotating the device, I would like the UINavigationBar to automatically apply the landscape UIBarMetrics properties (eg, change height, change the background image, resize the buttons etc), but by default, it does not. This is a problem on iOS 7, since even if I manually change the height of the UINavigationBar, the UIBarButtonItem elements don't change their vertical positions.
Is there a way to manually 'tell' the UINavigationBar to apply specific bar metric properties to itself? Or is that actually an implementation inside UINavigationController, and not UINavigationBar?
After various testing and trial and error after asking this question, I eventually worked out a solution that fixed all of my issues, so I'll post it here under the solution I'd previously accepted.
When my app is displayed in landscape on an iPhone, I wanted the UINavigationBar at the top to shrink to the standardly accepted 32 points high, as is the case with any apps that use the UINavigationController class. However, as I am not using a UINavigationController for this particular view (for varying reasons of feasibility), I needed to implement this manually.
To account for the new transparent status bar in iOS 7, I adjusted the origin and size of the UINavigationBar so it encompassed both the bounds of the status bar, and the normal UINavigationBar region (ie, so the UINavigationBar frame origin was {0,0}, and the height was 52 points.)
Unfortunately, this happened:
While the bar itself is rendering at the proper position and height, all of the content in the bar, including the title and buttons are not positioned properly, being much too high, almost touching the status bar content.
It was pretty obvious what was happening. The navigation bar content is being vertically aligned to its own middle, completely disregarding the presence of the status bar content.
When I tested the same orientations with a normal UINavigationController, this was not the case, and the title and buttons in the UINavigationBar from the UINavigationController worked absolutely fine. Apple had done SOMETHING in there that wasn't part of the normal UINavigationBar implementation.
Going on this, I picked apart the view layout hierarchy of a UINavigationControllerto see what was happening to the UINavigationBar in there (Mainly calling a lot of NSLog() statements that would dump the subviews of the navigation bar.)
This is what I discovered:
From the looks of it, Apple have employed a relatively sneaky hack to achieve this effect. It turns out the actual UINavigationBar is actually placed right below the status bar (ie at point {0,20}) and only has a height of 32 points. Then, what happens is a private subview inside the UINavigationBar in charge of rendering the background is extended upwards, outside of the bounds of the navigation bar to encompass the region behind the status bar (ie, its origin is {0, -20}, and its height is 52 points, local to the navigation bar's subview coordinate space).
So by doing that, not only does the content vertically align properly, but the translucent effect still extends behind the status bar.
Anyway, after I discovered this, it was pretty straightforward to write a solution. All I needed to do was reposition and resize the UINavigationBar back to how I had it iOS 6 (ie, 20 points down, and only 32 points high), and then implement a UINavigationBar subclass that override the layoutSubviews method, grabbed the internal background view (Doing a quick subview check for a view with a class name that matched "Background"), and then manually extended it.
The bar metrics properties you can set on a UINavigation bar are things like background image and the title vertical position. Heigh and width need to be set from within your view controller.
If you need to manually tell the navigation bar to change it's size when the orientation changes you can implement the method - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration1 in your view controller and change the size there.
Another option you can use is to use autolayout to specify that the width of your navigation bar is pinned to the left and right sides of its superview and let it figure out how wide it should be. For example
UINavigationBar *bar = [[UINavigationBar alloc] init];
bar.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:bar];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[bar]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(bar)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[bar(44)]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(bar)]];

Programmatically resizing a nib makes inactive area

I have an iPad app with a split view controller which implements some code to show/hide the tableview on the left.
The right hand side (the detail controller) is a custom UIView (specifically) which is loaded from a nib at runtime - this works fine.
If I hide the master then the detail resizes to fill the screen, as you'd expect, but an area suspiciously similar in width to that of the master tableview is then inactive along the far right of the detail - buttons placed there can not be clicked and you cannot scroll there.
I assume somehow that the resizing is causing this to happen but I cannot work out why...
As requested I use the following code to hide the master:
SplitViewController *splitViewController = (SplitViewController *)[[[[UIApplication sharedApplication] delegate] window] rootViewController];
splitViewController.shouldHideMasterController = !splitViewController.shouldHideMasterController;
[splitViewController willRotateToInterfaceOrientation:self.interfaceOrientation duration:0];
[splitViewController.view setNeedsLayout];
[self updateTextView];
I see this kind of behaviour happen a lot when developing iOS app, the thing to realise is that UIViews do not crop drawing of their subviews but will not let you interact with subview that are outs side their bounds. Check that your content that you can not click on is not outside the frame of any of its superviews.

How do I make self.view the full height of the view controller (occupies height of navigation bar and status bar as well)

Right now, self.view is the height of the view controller minus the height of the nav bar and status bar, so when I hide these it messes my auto layout constraints up. Given how often the nav bar and status bar are hidden and unhidden, it would be easier just to give the view controller full height (but still allow the nav bar and status bar to be there at times), how exactly would I do this?
I'm not sure if I totally follow what you want, but I think it's something along these lines:
- (void) viewDidLoad
{
[super viewDidLoad]
self.view.frame = [[UIScreen mainScreen] bounds];
// You may or may not want to reference [[UIDevice currentDevice] orientation], too
// Other stuff, whatever you want to do...
}
Note, too, that you can turn-off auto-resize and constraints, if the problem you're having is related to those.
In general, the view controller's view not-including the nav-bar, etc. isn't a good idea. If you're having trouble because of that, you might want to consider another design.

resize content after iPhone status bar is shown/hidden

i have an iPhone application which I want to start with hidden status bar, to show a splash screen, and switch the status bar back in when the content to be shown is loaded (it takes some times since it has to be loaded over the internet), using [[UIApplication sharedApplication] setStatusBarHidden:NO animated:YES]. I want my content view to resize and position itself properly after status bar is shown and hidden. I want my application to be able to run in any device orientation (maybe excluding the topdown orientation as specified in apple guidelines).
My problem is I can't get the application to appear as it should. Either it shows a 20pt blank space after autorotation or is hidden below the status bar when it appears.
What should be the proper way to handle this?
I haven't tested it yet, but i guess same issues would arise by the incoming call bar.
EDIT:
I'll try to explain better what the problem is. When the application starts it has status bar hidden, and in the window coordinate system the top corner of the window's visible area is 0,0 in the window's coordinate system. Then I slide in status bar. This doesn't change window size, hence 0,0 point is now hidden behind status bar. Top left of visible area of the window is 0,20 and I have to move my view to those coordinates (and resize it properly to 320 * 460). But if I rotate the device, then back to the original position, top left coordinate of visible window area becomes 0,0 again, and the window is now sized at 320 * 480.
A workaround is to keep a flag telling whether the phone has been rotated before, but isn't there a way to have the window coordinates not changing when I rotate to another position and then back?
Yes, the same issues come up with the call bar. Alternatively, the audio recording bar does the same thing. (Its easier and maybe cheaper to start the voice memo app recording and come back to your app to test how it handles the extra bar.)
Setting the auto-resize masks on the view is what usually adjusts things for you.
If you are building your UI in XIB, you can play with the settings until your views properly change size when the bar changes or when the device or simulator rotates.
Remember that the mask settings on the contained subviews are important too.
Does #walt's answer really works? I've tried to add this flag to every view controller's view property, but without any luck there, am I missing sth?
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
This solution solved my problem, but it looks ugly. I have to resize the view controller view frame manually.
This should fix your issue
Just a View:
[self.view setFrame: [self.view bounds]];
A view with a scroll view inside
[self.view setFrame: [self.view bounds]];
[self.theScroller setFrame: [self.view bounds]];
"theScroller is the name of my scrollview

iPad Modal UIViewControllers position

How do I position this ViewController over the detail view modally? I want it to be right aligned so you can see the navigation portion greyed out.
[self.window addSubview:self.splitViewController.view];
MyModalViewController *modalvc= [MyModalViewController new]; //brevity
modalvc.modalInPopover = YES;
modalvc.modalPresentationStyle = UIModalPresentationStylePageSheet;
modalvc.view.autoresizeMask = UIViewAutoresizeFlexibleRightMargin;
[self.splitViewController presentModalView:modalvc animated:NO];
[self.window makeKeyAndVisible];
I also checked in MyModalViewController if im setting the views mask and I am not, nor is it getting magic values from a Nib.
Adjusting the frame before the present (using modalvc.view.frame) does nothing.
Adjusting the frame after the present seems to yield crazy results, and I only really need it to be half a width over in landscape... portrait is normal behavior.
edit
the picture confused people so I took it out, I dont want the modal view to be the size of the screen, I want to keep it UIModalPresentationStylePageSheet but have its ORIGIN moved right so that it covers the detail view portion in landscape
The modalPresentationStyle is what controls that. You've set it to UIModalPresentationStylePageSheet, which sets the height to the height of the screen, and the width to to the width of the screen in portrait orientation, exactly as in your screenshot.
I think the only way to get full width in landscape is to use UIModalPresentationFullScreen. See the UIViewController reference for more info.
I had a similar problem, and I ended up using a custom view controller that uses a background translucent view and a foreground opaque view that I am able to position anywhere I want by manipulating its frame. It's useful as a lightbox for videos and images.

Resources