A headache-inducing scenario: I’m working on a view controller, and I realise that in order to support landscape and portrait modes, I’m going to need to provide two different layouts.
So, I create two different views within the nib, one portrait, one landscape, each with the same view hierarchy, but with a different layout.
When the orientation changes, I set self.view
to the appropriate view. I initialise both views on load, and keep both of them synced to properly reflect the app’s state — basically, I’m double-handling everything, which bloats my code and increases the chance I’ll make a mistake.
So, here’s an easier way: Rather than maintaining two separate view hierarchies and switching between them when the orientation changes, why not just change the layout of one single view hierarchy? The only changes between the portrait and landscape views are layout changes, so if we can extract just the layout information from each view, then we don’t have to worry about maintaining both view hierarchies.
Basically, we’re talking about using each view version as a layout template only.
That’s what [TPMultiLayoutViewController](https://github.com/michaeltyson/TPMultiLayoutViewController) class does. It’s a drop-in UIViewController subclass that automatically manages switching between different view layouts for portrait and landscape orientations, without the need to maintain view state across two different view hierarchies.
It works by defining portraitView
and landscapeView
outlets which it traverses upon loading the nib. It matches each subview element to its counterpart in the other layout (based on tag, target/action, title, etc.), and stores just the layout attributes of each element.
Then, when the orientation changes, the view hierarchy is traversed and these layouts are applied to each subview.
To use it,
- Set the superclass for your view controller to
TPMultiLayoutViewController
. - In Interface Builder, create two different views: one for portrait orientation, and one for landscape orientation.
- Attach your portrait orientation root view to the “portraitView” outlet, and the landscape orientation root view to the “landscapeView” outlet.
- Attach one of the views (whichever you prefer) to the “view” outlet, and connect any actions and outlets from that view.
Grab it from the [TPMultiLayoutViewController GitHub repository](https://github.com/michaeltyson/TPMultiLayoutViewController), and let me know what you think.
OMG – I have been looking for a piece on this for so long. You are a god, no, demi-god.
That is seriously sweet utility code – this’ll save me from managing two different versions of the view manually. Thanks!
Haha! Oh, you.
Brilliant, works like a charm!
I ran into one issue when using this. I have a label that’s set to adjust to the text. After rotating from portrait to landscape, the width of the label expands to fit the parent UIView and the text reflows to fit into the width, however, the height stays the same causing a lot of space above and below the text. This happens when I use TPMultiLayoutViewController. The code for the label inside my view controller:
heading.text = title; heading.numberOfLines = 0; heading.backgroundColor = [UIColor yellowColor]; heading.lineBreakMode = UILineBreakModeWordWrap; heading.font = [UIFont fontWithName:@"Baskerville" size:36]; heading.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); [heading sizeToFit];
Is there a modification that can be made in TPMultiLayoutViewController.m because it works fine otherwise?
(Side note: as a quick fix, I tried creating MyLabel that extends UILabel where I implement willAnimateRotationToInterfaceOrientation and call sizeToFit and set a break point to see if it gets called, but it doesn’t, though this is unrelated to TPMultiLayoutViewController).
Hey,
I think the best way to fix the label after the layout change is to override
- (void)applyLayoutForInterfaceOrientation:(UIInterfaceOrientation)newOrientation;
in the TPMultiLayoutViewController subclass, call[super applyLayoutForInterfaceOrientation:newOrientation]
, and then set the frame of the label as appropriate.Cheers =)
This almost works perfectly for me. It finds everything at the top-level of the xib perfectly fine. However, I have a toolbar and it can’t seem to match the items in the toolbar, even though I set matching tags for each of the toolbar items. It also doesn’t match for a UIImageView inside a UIScrollView. Is it intended to dig deep into the hierarchy, or will I have to modify the class a bit?
(Example output) Couldn’t find match for UIView => UIToolbar => UIToolbarTextButton Couldn’t find match for UIView => UIScrollView => UIImageView
Hey John,
Hmm, without actually digging into your project, I’m not sure what’s going on – mind you, those messages are only warnings themselves, not necessarily a sign that it’s not working. Perhaps it’s just finding some views managed by the system, which happens from time to time – it’ll just ignore these, and let the system manage those views.
So, does it look OK when you change orientations? If yes, then you’re all set.
Hi Michael,
Lovely idea.
Problem with iOS5 and Arc is that I have two lines in your code that require a ‘bridged cast’. I am fairly new to programming so I’d appreciate it if you could tell me how to fix these two lines:
NSDictionary *attributes = [table objectForKey:[NSValue valueWithPointer:view]];
[table setObject:[self attributesForView:view] forKey:[NSValue valueWithPointer:associatedView]];
Thanks,
Dave
Hey Dave, You’ll need:
NSDictionary *attributes = [table objectForKey:[NSValue valueWithPointer:(__bridge void*)view]]; [table setObject:[self attributesForView:view] forKey:[NSValue valueWithPointer:(__bridge void*)associatedView]];
Thanks Michael,
I worked that out a short while after posting but for some reason I couldn’t repost or edit my post to let you know!
Cheers. Great post
Dave
Cool =) And cheers!
I’ve used this class in an app I’ve had for sale for 2 years, works great.
I’m refactoring the app and attempting to use storyboards. Can’t get it working there. Basically in the storyboard I can’t get two different views attached to the one view controller. Bummer! Apple’s method of supporting both portrait and landscape views does not work properly if you are using a navigation controller.
Any suggestions?
Thanks, Ken
I just found this bit of code. Great idea and it works pretty well. However I can’t figure out how to get it to support both iPhone’s with 3.5 in screens and the 4 inch screeens. Any ideas?
Andrew
Hi..
This seems a great work u have done!
But unfortunately at my end I am not able to see the result. I created simple project with only ine label in xib for both orientations. But it is showing me content of only one orientation. It is not updating views after orientation is changed.
Do you have any idea about why is this happening?
Thanks in advance.
Asawari
Hey… I got the problem. In my case ‘Auto Layout’ property was enabled. So views were not reflecting according to device orientation.
Its a great work you have done!
Thank you!
Love the concept! Alas, it doesn’t appear to work with autolayout. (Or, rather, I try out of the box and the layout breaks spectacularly, but I’m not sure why yet.) Clues welcome/appreciated!