Problems with multiple xibs for rotation under iOS6
Asked Answered
H

3

2

I need to use different xib files for portrait and landscape. I am not using Auto Layout but I am using iOS6. (See my previous question if you care why.)

I'm following Adam's answer to this question modified with amergin's initWithNib name trick, modified with my own iPhone/iPad needs. Here's my code:

-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{    
    [[NSBundle mainBundle] loadNibNamed:[self xibNameForDeviceAndRotation:toInterfaceOrientation]
                                  owner: self
                                options: nil];
    [self viewDidLoad];
}

- (NSString *) xibNameForDeviceAndRotation:(UIInterfaceOrientation)toInterfaceOrientation
{
    NSString *xibName ;
    NSString *deviceName ;

    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        deviceName = @"iPad";
    } else {
        deviceName = @"iPhone";
    }

    if( UIInterfaceOrientationIsLandscape(toInterfaceOrientation) )
    {
        xibName = [NSString stringWithFormat:@"%@-Landscape", NSStringFromClass([self class])];
        if([[NSBundle mainBundle] pathForResource:xibName ofType:@"nib"] != nil) {
            return xibName;
        } else {
            xibName = [NSString stringWithFormat:@"%@_%@-Landscape", NSStringFromClass([self class]), deviceName];
            if([[NSBundle mainBundle] pathForResource:xibName ofType:@"nib"] != nil) {
                return xibName;
            } else {
                NSAssert(FALSE, @"Missing xib");
                return nil;
            }
        }

    } else {
        xibName = [NSString stringWithFormat:@"%@", NSStringFromClass([self class])];
        if([[NSBundle mainBundle] pathForResource:xibName ofType:@"nib"] != nil) {
            return xibName;
        } else {
            xibName = [NSString stringWithFormat:@"%@_%@", NSStringFromClass([self class]), deviceName];
            if([[NSBundle mainBundle] pathForResource:xibName ofType:@"nib"] != nil) {
                return xibName;
            } else {
                NSAssert(FALSE, @"Missing xib");
                return nil;
            }
        }
    }
}

and of course I'm doing:

- (BOOL) shouldAutorotate
{
    return YES;
}

- (NSUInteger)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskAll;
}

in my view controller and:

- (NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
    return (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskPortraitUpsideDown);
}

in my delegate.

I have two problems which may be related. First, the easy one. I do not rotate upside down. I have all all the proper bits turned on in xcode for both iPad and iPhone. This may be a separate issue or it may be the core of my problem.

The real problem is that when I rotate to landscape mode my xib is replace but the view is off by 90 degrees.

Here's what my 2 xib's look like. (I've colored them garishly so you can see that they are different.)

enter image description here and enter image description here

and you can see when I run it (initially in Landscape mode) that the landscape xib is correct.

enter image description here

when I rotate to portrait it is also correct

enter image description here

but when I rotate back to landscape the xib is replaced but the view is off by 90 degrees.

enter image description here

What's wrong here?

Hailstone answered 2/7, 2013 at 20:35 Comment(7)
I think you are loading the xib to early, did you try to move the code from willRotatetToInterface.. into didRotateFrom... ?Garfieldgarfinkel
hmmmm, that gives me the from orientation. Since there are essentially only 2 possible ones for my app I guess it is ok, but it seems unclean...Hailstone
and it also doesn't work. The magenta is now full width but the UI widgets are still off by 90 degrees.Hailstone
Hi, did you find a solution to your problem, I'm currently facing the same and the only way I found is to remove the 2nd xib, and making all the frame changes inside the code... :*(Recurrence
Yes, I have, just finished it up yesterday or so. Need to write it up in my blog. It was both easy and not. In one sentence, I used container view controllers.Hailstone
ecaste, I've answered the question now. Hope the technique works for you!Hailstone
I'm having the same issue but find it hard to believe that so many others are saying this works correctly for them. What gives? I don't really want to go as far as you did, with the view switching design.Caddaric
H
0

I'm answering my own question here pasting from the full article on my iOS blog at http://www.notthepainter.com/topologically-challenged-ui/

I had a friend help me out, he used 2 views in one xib file with IBOutlets for portrait and landscape view and he toggled between them the device rotated. Perfect, right? Well, no, when you have 2 views in a XIB you can’t hook up your IBOutlets to both places. I had it working visually but my controls only worked in one orientation.

I eventually came up with the idea of using a orientation master view controller that loaded container view controllers when the device rotated. That worked fine. Lets look at the code:

-(void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation duration:(NSTimeInterval)duration
{
    if (_timerViewController) {
        [_timerViewController.view removeFromSuperview];
        [_timerViewController willMoveToParentViewController:nil];
        [_timerViewController removeFromParentViewController];
        self.timerViewController = nil;
    }

    self.timerViewController = [[XTMViewController alloc] initWithNibName:
          [self xibNameForDeviceAndRotation:interfaceOrientation withClass:[XTMViewController class]]
                                             bundle:nil];

    // use bounds not frame since frame doesn't take the status bar into account
    _timerViewController.view.frame = _timerViewController.view.bounds = self.view.bounds;

    [self addChildViewController:_timerViewController];
    [_timerViewController didMoveToParentViewController:self];
    [self.view addSubview: _timerViewController.view];
}

The addChildViewController and didMoveToParentViewController should be familiar if you read my previous blog entry on Container View Controllers. There are two things to notice above those calls though. I’ll deal with the second one first, I set the child view controller’s frame and bounds from the parents bounds, not frame. This is to take account of the status bar.

And notice the call to xibNameForDeviceAndRotation to load the view controller from its xib file. Lets look at that code:

- (NSString *) xibNameForDeviceAndRotation:(UIInterfaceOrientation)toInterfaceOrientation withClass:(Class) class;
{
    NSString *xibName ;
    NSString *deviceName ;

    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        deviceName = @"iPad";
    } else {
        deviceName = @"iPhone";
    }

    if( UIInterfaceOrientationIsLandscape(toInterfaceOrientation) )  {
        xibName = [NSString stringWithFormat:@"%@-Landscape", NSStringFromClass(class)];
        if([[NSBundle mainBundle] pathForResource:xibName ofType:@"nib"] != nil) {
            return xibName;
        } else {
            xibName = [NSString stringWithFormat:@"%@_%@-Landscape", NSStringFromClass(class), deviceName];
            if([[NSBundle mainBundle] pathForResource:xibName ofType:@"nib"] != nil) {
                return xibName;
            } else {
                NSAssert(FALSE, @"Missing xib");
                return nil;
            }
        }
    } else {
        xibName = [NSString stringWithFormat:@"%@", NSStringFromClass(class)];
        if([[NSBundle mainBundle] pathForResource:xibName ofType:@"nib"] != nil) {
            return xibName;
        } else {
            xibName = [NSString stringWithFormat:@"%@_%@", NSStringFromClass(class), deviceName];
            if([[NSBundle mainBundle] pathForResource:xibName ofType:@"nib"] != nil) {
                return xibName;
            } else {
                NSAssert(FALSE, @"Missing xib");
                return nil;
            }
        }
    }
    return nil;
}

There’s a lot going on here. Let’s go over it. I first determine if you are on an iPhone or an iPad. The xib files will have iPhone or iPad in their names. Next we check to see if we are in landscape mode. If we are, we build a test string from the class name, using class reflection via NSStringFromClass. Next, we use pathForResource to check to see if the xib exists in our bundle. If it does, we return the xib name. If it doesn’t, we try again also putting the device name into the xib name. Return it if it exists, assert a failure if it doesn’t. Portrait is similar except by convention we don’t put “-Portrait” into the xib name.

This code is useful enough and generic enough that I’ll put it in my EnkiUtils open source project.

Since this is iOS6 we need to put in the iOS6 rotation boilerplate code:

- (BOOL) shouldAutorotate
{
    return YES;
}

- (NSUInteger)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskAll;
}

Curiously we also need to manually call willAnimateRotationToInterfaceOrientation on iPads. iPhones get a willAnimateRotationToInterfaceOrientation automatically but iPads do not.

- (void) viewDidAppear:(BOOL)animated
{
    // iPad's don't send a willAnimate on launch...
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        [self willAnimateRotationToInterfaceOrientation:[[UIApplication sharedApplication] statusBarOrientation] duration:0];
    }
}

So, are we finished? Embarrassingly no. You see, when I coded the XTMViewController class I broke the Model-View-Controller design pattern! This is easy to do, Apple already helps us by putting the View and the Controller into the same class. And it is so easy to carelessly mix in Model data in the VC’s .h file. And I had done exactly that. When I run the above code it work brilliantly, I could rotate it all day and the UI was correct in both orientations. But what do you think happened when I rotated the device while my exercise timers were running? Yup, they were all deleted and the UI reset to the initial state. This was not at all what I wanted!

I made a XTMUser class to hold all the timing data, I put all the NSTimers into the XTMOrientationMasterViewController class and then I made a protocol so the XTMOrientationMasterViewController could respond to UI taps in the XTMViewController class.

Then I was done.

Hailstone answered 23/7, 2013 at 16:51 Comment(0)
C
1

I've been following probably the same path as Paul Cezanne did last year. Not sure if he tried this or not, but I solved the original issue (stated in this question) by just making my root controller a navigation controller instead of my view controller class. Since I'm using an "empty project" template and XIB files, this meant changing the normal:

self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
self.window.rootViewController = self.viewController;

inside AppDelegate.m, to this instead:

self.viewController = [[ViewController alloc] initWithNibName:@"ViewController" bundle:nil];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:self.viewController];
navigationController.navigationBar.hidden = YES;
self.window.rootViewController = navigationController;

That is, I just created a generic UINavigationController and set that as the root view controller.

I'm not sure if this will cause other problems, and there is probably a way to figure out (maybe you would need the source code though) what UINavigationController does that UIViewController doesn't. Could be as simple as one extra setNeedsLayout type of call in the right place. If I figure it out, I'll edit this answer for future readers.

Credit goes to Sakti's comments on Easiest way to support multiple orientations? How do I load a custom NIB when the application is in Landscape? which I shouldn't have ignored the first time I read them:

i added the view controller to navigation controller and presented it which made it work as intended

Edit: Added extra line to example code to hide navigation bar, since most people following this issue will not want that.

Caddaric answered 12/9, 2014 at 21:12 Comment(1)
Interesting approach. If it works for me it would be much much simpler. Of course, that particular project is long shipped but I just might open it up again to try. I'll upvote just because it is clever, don't know if it will work or not. Thanks!Hailstone
B
0

This is how I do it and it works on iOS 5+:

   - (void)viewDidLoad {
   [[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(checkBagRotation)
                                             name:UIApplicationDidChangeStatusBarOrientationNotification
                                           object:nil];
  [self checkBagRotation];
 }

- (void)checkBagRotation {
    orientation = [UIApplication sharedApplication].statusBarOrientation;
    if(orientation == UIInterfaceOrientationLandscapeLeft || orientation == UIInterfaceOrientationLandscapeRight) {
        [[NSBundle mainBundle] loadNibNamed:@"Controller-landscape"
                                      owner:self
                                    options:nil];
   } else {
        [[NSBundle mainBundle] loadNibNamed:@"Controller-portrait"
                                      owner:self
                                    options:nil];

}
Bacciform answered 3/7, 2013 at 13:18 Comment(1)
I'll give that a shot. I would have thought the delegate methods would have worked and I'm pretty sure my code above works under iOS5...Hailstone
H
0

I'm answering my own question here pasting from the full article on my iOS blog at http://www.notthepainter.com/topologically-challenged-ui/

I had a friend help me out, he used 2 views in one xib file with IBOutlets for portrait and landscape view and he toggled between them the device rotated. Perfect, right? Well, no, when you have 2 views in a XIB you can’t hook up your IBOutlets to both places. I had it working visually but my controls only worked in one orientation.

I eventually came up with the idea of using a orientation master view controller that loaded container view controllers when the device rotated. That worked fine. Lets look at the code:

-(void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation duration:(NSTimeInterval)duration
{
    if (_timerViewController) {
        [_timerViewController.view removeFromSuperview];
        [_timerViewController willMoveToParentViewController:nil];
        [_timerViewController removeFromParentViewController];
        self.timerViewController = nil;
    }

    self.timerViewController = [[XTMViewController alloc] initWithNibName:
          [self xibNameForDeviceAndRotation:interfaceOrientation withClass:[XTMViewController class]]
                                             bundle:nil];

    // use bounds not frame since frame doesn't take the status bar into account
    _timerViewController.view.frame = _timerViewController.view.bounds = self.view.bounds;

    [self addChildViewController:_timerViewController];
    [_timerViewController didMoveToParentViewController:self];
    [self.view addSubview: _timerViewController.view];
}

The addChildViewController and didMoveToParentViewController should be familiar if you read my previous blog entry on Container View Controllers. There are two things to notice above those calls though. I’ll deal with the second one first, I set the child view controller’s frame and bounds from the parents bounds, not frame. This is to take account of the status bar.

And notice the call to xibNameForDeviceAndRotation to load the view controller from its xib file. Lets look at that code:

- (NSString *) xibNameForDeviceAndRotation:(UIInterfaceOrientation)toInterfaceOrientation withClass:(Class) class;
{
    NSString *xibName ;
    NSString *deviceName ;

    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        deviceName = @"iPad";
    } else {
        deviceName = @"iPhone";
    }

    if( UIInterfaceOrientationIsLandscape(toInterfaceOrientation) )  {
        xibName = [NSString stringWithFormat:@"%@-Landscape", NSStringFromClass(class)];
        if([[NSBundle mainBundle] pathForResource:xibName ofType:@"nib"] != nil) {
            return xibName;
        } else {
            xibName = [NSString stringWithFormat:@"%@_%@-Landscape", NSStringFromClass(class), deviceName];
            if([[NSBundle mainBundle] pathForResource:xibName ofType:@"nib"] != nil) {
                return xibName;
            } else {
                NSAssert(FALSE, @"Missing xib");
                return nil;
            }
        }
    } else {
        xibName = [NSString stringWithFormat:@"%@", NSStringFromClass(class)];
        if([[NSBundle mainBundle] pathForResource:xibName ofType:@"nib"] != nil) {
            return xibName;
        } else {
            xibName = [NSString stringWithFormat:@"%@_%@", NSStringFromClass(class), deviceName];
            if([[NSBundle mainBundle] pathForResource:xibName ofType:@"nib"] != nil) {
                return xibName;
            } else {
                NSAssert(FALSE, @"Missing xib");
                return nil;
            }
        }
    }
    return nil;
}

There’s a lot going on here. Let’s go over it. I first determine if you are on an iPhone or an iPad. The xib files will have iPhone or iPad in their names. Next we check to see if we are in landscape mode. If we are, we build a test string from the class name, using class reflection via NSStringFromClass. Next, we use pathForResource to check to see if the xib exists in our bundle. If it does, we return the xib name. If it doesn’t, we try again also putting the device name into the xib name. Return it if it exists, assert a failure if it doesn’t. Portrait is similar except by convention we don’t put “-Portrait” into the xib name.

This code is useful enough and generic enough that I’ll put it in my EnkiUtils open source project.

Since this is iOS6 we need to put in the iOS6 rotation boilerplate code:

- (BOOL) shouldAutorotate
{
    return YES;
}

- (NSUInteger)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskAll;
}

Curiously we also need to manually call willAnimateRotationToInterfaceOrientation on iPads. iPhones get a willAnimateRotationToInterfaceOrientation automatically but iPads do not.

- (void) viewDidAppear:(BOOL)animated
{
    // iPad's don't send a willAnimate on launch...
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        [self willAnimateRotationToInterfaceOrientation:[[UIApplication sharedApplication] statusBarOrientation] duration:0];
    }
}

So, are we finished? Embarrassingly no. You see, when I coded the XTMViewController class I broke the Model-View-Controller design pattern! This is easy to do, Apple already helps us by putting the View and the Controller into the same class. And it is so easy to carelessly mix in Model data in the VC’s .h file. And I had done exactly that. When I run the above code it work brilliantly, I could rotate it all day and the UI was correct in both orientations. But what do you think happened when I rotated the device while my exercise timers were running? Yup, they were all deleted and the UI reset to the initial state. This was not at all what I wanted!

I made a XTMUser class to hold all the timing data, I put all the NSTimers into the XTMOrientationMasterViewController class and then I made a protocol so the XTMOrientationMasterViewController could respond to UI taps in the XTMViewController class.

Then I was done.

Hailstone answered 23/7, 2013 at 16:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.