Search code examples
iosios6autorotate

Problems with multiple xibs for rotation under iOS6


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?


Solution

  • 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.