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.)
and
and you can see when I run it (initially in Landscape mode) that the landscape xib is correct.
when I rotate to portrait it is also correct
but when I rotate back to landscape the xib is replaced but the view is off by 90 degrees.
What's wrong here?
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.
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];
}
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.