Uiviewcontroller Returns Invalid Frame

UIViewController returns invalid frame?

There are a couple of things that you don't understand.

First, the system sends you viewDidLoad immediately after loading your nib. It hasn't even added the view to the view hierarchy yet. So it hasn't resized your view based on the device's rotation either.

Second, a view's frame is in its superview's coordinate space. If this is your root view, its superview will be the UIWindow (once the system actually adds your view to the view hierarchy). The UIWindow handles rotation by setting the transform of its subview. This mean that the view's frame will not necessarily be what you expect.

Here's the view hierarchy in portrait orientation:

(lldb) po [[UIApp keyWindow] recursiveDescription]
(id) $1 = 0x09532dc0 >
| >

and here's the view hierarchy in landscape-left orientation:

(lldb) po [[UIApp keyWindow] recursiveDescription]
(id) $2 = 0x09635e70 >
| >

Notice that in landscape orientation, the frame size is 748 x 1024, not 1024 x 748.

What you probably want to look at, if this is your root view, is the view's bounds:

(lldb) p (CGRect)[0x9634ee0 bounds]
(CGRect) $3 = {
(CGPoint) origin = {
(CGFloat) x = 0
(CGFloat) y = 0
}
(CGSize) size = {
(CGFloat) width = 1024
(CGFloat) height = 748
}
}

Presumably you want to know when the view's transform, frame, and bounds get updated. If the interface is in a landscape orientation when your view controller loads its view, you will receive messages in this order:

{{0, 0}, {768, 1004}} viewDidLoad
{{0, 0}, {768, 1004}} shouldAutorotateToInterfaceOrientation:
{{0, 0}, {768, 1004}} shouldAutorotateToInterfaceOrientation:
{{0, 0}, {768, 1004}} viewWillAppear:
{{0, 0}, {768, 1004}} shouldAutorotateToInterfaceOrientation:
{{0, 0}, {768, 1004}} shouldAutorotateToInterfaceOrientation:
{{0, 0}, {768, 1004}} willRotateToInterfaceOrientation:duration:
{{0, 0}, {1024, 748}} viewWillLayoutSubviews
{{0, 0}, {1024, 748}} layoutSubviews
{{0, 0}, {1024, 748}} viewDidLayoutSubviews
{{0, 0}, {1024, 748}} willAnimateRotationToInterfaceOrientation:duration:
{{0, 0}, {1024, 748}} shouldAutorotateToInterfaceOrientation:
{{0, 0}, {1024, 748}} viewDidAppear:

You can see that your view's bounds change after you receive willRotateToInterfaceOrientation:duration: and before you receive viewWillLayoutSubviews.

The viewWillLayoutSubviews and viewDidLayoutSubviews methods are new to iOS 5.0.

The layoutSubviews message is sent to the view, not the view controller, so you will need to create a custom UIView subclass if you want to use it.

Unable to set frame correctly before viewDidAppear

viewDidLoad is called when the class is loaded however no ui elements have been initialised and therefore any attempt to reference them will be overwritten or unavaliable during the initialisation process which happens between the viewDidLoad and viewDidAppear calls. Once all ui element have been initalised and drawn viewDidAppear is called.

viewDidLoad -
Called after the controller's view is loaded into memory

At this point the view isn't within the view hierarchy.

viewWillAppear - Notifies the view controller that its view is about to be added to a view hierarchy.

Again, the view is yet to be added to the view hierarchy.

viewDidAppear - Notifies the view controller that its view was added to a view hierarchy.

Only then is the view added to the view hierarchy.

Update

The viewDidLayoutSubviews is the most appropriate place to modify the UI before it actually appears on the screen.

viewDidLayoutSubviews - Notifies the view controller that its view just laid out its subviews.

UIKeyboard adopts an invalid frame

After 3 months, I finally figured it out (I've been doing other stuff, FYI).

The short answer

Make sure you're not returning crap from any rotation method, otherwise keyboard goes loco.

The long version

Discovered by using the application Reveal, that draws a 3d decomposition of each layer in the app. I noticed the keyboard was being presented but it's frame was landscape, when the interface was in portrait. This immediately made me revise the whole way the rotation is being handled.

The whole problem spans from a UIViewController that posses child view controllers with a complex structure, and the rotation calls are getting forwarded to the children manually. For example, this method:

- (NSUInteger)supportedInterfaceOrientations
{
return [[[(id)self.selectedViewController viewControllers] lastObject] supportedInterfaceOrientations];
}

Was relying on whatever top-most view controller on the current selected navigation controller to return the value. Well, one of the view controllers went rogue, it wasn't returning anything here, so the return value must've been just about anything in memory or zero.

The solution now checks if the view controllers responds to the method, and if not, it defaults to portrait (which is about 90% of the app).

- (NSUInteger)supportedInterfaceOrientations
{
NSUInteger supported = UIInterfaceOrientationMaskPortrait;
UIViewController *viewController = [[(id)self.selectedViewController viewControllers] lastObject];

if (viewController && [viewController respondsToSelector:@selector(supportedInterfaceOrientations)])
supported = [viewController supportedInterfaceOrientations];

return supported;
}

Btw, the sane way to do it would be to just return YES from -(BOOL)shouldAutomaticallyForwardRotationMethods (iOS 6, 7) but this was an impossibility due to how it was implemented. For now, it's on NO and being manually handled.

There's still the question why it only mattered with presented view controllers. Although, I rather just discard it as "random behavior from returning an invalid interface orientation".

UIViewController/UIView orientation frame/bounds in landscape only app

Not sure if this is right, but it's my best guess and I'm unable to test at this moment. If I'm not mistaking, the default orientation is portrait for any application. So, in order to support different orientations, your application should implement the auto rotation methods (are a bit different depending on the iOS version you're building on). So even if you application is ticked to only support landscape mode, it never actually rotates. Try implementing the specified methods and let me know how it goes...

for iOS 5 and earlier you should use:

-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
{
if (toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft || toInterfaceOrientation == UIInterfaceOrientationLandscapeRight)
{
return YES;
}

return NO;
}

for iOS 6 and later you should use:

-(NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskLandscape;
}

-(BOOL)shouldAutorotate
{
return YES;
}

If you check the view's frame after the rotation takes place, it should be allright.

EDIT:

Take a look at this SO question and it's answers. They provide some nice workarounds. Also, given that in an application you will mostly have your view controllers embedded in a navigation controller or tab bar controller or even both, you can go ahead and subclass/create category on them to make sure that everything is forwarded to your view controllers.

Another great answer explains what's actually happening.

UIViewController viewDidLoad incorrect width/height

If you are targetting iOS 5.0 or better you can use viewWillLayoutSubviews and viewDidLayoutSubviews to make changes.

As for your second question, if you need access to an instance variable in other method than init, you need to keep it around, I don't see a problem with it.

You can, however, try to use Auto Layouts and set up rules between the subviews so it's automatically laid out for you without the need to keep a reference.

How to control a UIViewController's view frame

Your calling of viewController.view (followed by .frame) is what triggers the call to viewDidLoad.

The proper solution is to layout the subviews in the viewWillLayoutSubviews method of your view controller, not in viewDidLoad.



Related Topics



Leave a reply



Submit