Sizing class for iPad portrait and Landscape Modes
Asked Answered
E

7

100

I basically want to have my subviews positioned differently depending upon the orientation of the iPad (Portrait or Landscape) using Sizing Classes introduced in xcode 6. I have found numerous tutorials explaining how different sizing classes are available for Iphones in portrait and landscape on the IB but however there seem to be none that cover individual landscape or portrait modes for the iPad on IB. Can anyone help?

Ekaterinoslav answered 29/10, 2014 at 14:37 Comment(1)
There appear to not be a non-programmatic solution, which would be needed for the Default screenBoil
M
176

It appears to be Apple's intent to treat both iPad orientations as the same -- but as a number of us are finding, there are very legitimate design reasons to want to vary the UI layout for iPad Portrait vs. iPad Landscape.

Unfortunately, the current OS doesn't seem to provide support for this distinction ... meaning that we're back to manipulating auto-layout constraints in code or similar workarounds to achieve what we should ideally be able to get for free using Adaptive UI.

Not an elegant solution.

Isn't there a way to leverage the magic that Apple's already built into IB and UIKit to use a size class of our choosing for a given orientation?

~

In thinking about the problem more generically, I realized that 'size classes' are simply ways to address multiple layouts that are stored in IB, so that they can be called up as needed at runtime.

In fact, a 'size class' is really just a pair of enum values. From UIInterface.h:

typedef NS_ENUM(NSInteger, UIUserInterfaceSizeClass) {
    UIUserInterfaceSizeClassUnspecified = 0,
    UIUserInterfaceSizeClassCompact     = 1,
    UIUserInterfaceSizeClassRegular     = 2,
} NS_ENUM_AVAILABLE_IOS(8_0);

So regardless of what Apple has decided to name these different variations, fundamentally, they're just a pair of integers used as a unique identifier of sorts, to distinguish one layout from another, stored in IB.

Now, supposing that we create an alternate layout (using a unused size class) in IB -- say, for iPad Portrait ... is there a way to have the device use our choice of size class (UI layout) as needed at runtime?

After trying several different (less elegant) approaches to the problem, I suspected there might be a way to override the default size class programmatically. And there is (in UIViewController.h):

// Call to modify the trait collection for child view controllers.
- (void)setOverrideTraitCollection:(UITraitCollection *)collection forChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);

Thus, if you can package your view controller hierarchy as a 'child' view controller, and add it to a top-level parent view controller ... then you can conditionally override the child into thinking that it's a different size class than the default from the OS.

Here's a sample implementation that does this, in the 'parent' view controller:

@interface RDTraitCollectionOverrideViewController : UIViewController {
    BOOL _willTransitionToPortrait;
    UITraitCollection *_traitCollection_CompactRegular;
    UITraitCollection *_traitCollection_AnyAny;
}
@end

@implementation RDTraitCollectionOverrideViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setUpReferenceSizeClasses];
}

- (void)setUpReferenceSizeClasses {
    UITraitCollection *traitCollection_hCompact = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
    UITraitCollection *traitCollection_vRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];
    _traitCollection_CompactRegular = [UITraitCollection traitCollectionWithTraitsFromCollections:@[traitCollection_hCompact, traitCollection_vRegular]];

    UITraitCollection *traitCollection_hAny = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassUnspecified];
    UITraitCollection *traitCollection_vAny = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassUnspecified];
    _traitCollection_AnyAny = [UITraitCollection traitCollectionWithTraitsFromCollections:@[traitCollection_hAny, traitCollection_vAny]];
}

-(void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    _willTransitionToPortrait = self.view.frame.size.height > self.view.frame.size.width;
}

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]
    _willTransitionToPortrait = size.height > size.width;
}

-(UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController {
    UITraitCollection *traitCollectionForOverride = _willTransitionToPortrait ? _traitCollection_CompactRegular : _traitCollection_AnyAny;
    return traitCollectionForOverride;
}
@end

As a quick demo to see whether it worked, I added custom labels specifically to the 'Regular/Regular' and 'Compact/Regular' versions of the child controller layout in IB:

enter image description here enter image description here

And here's what it looks like running, when the iPad is in both orientations: enter image description here enter image description here

Voila! Custom size class configurations at runtime.

Hopefully Apple will make this unnecessary in the next version of the OS. In the meantime, this may be a more elegant and scalable approach than programmatically messing with auto-layout constraints or doing other manipulations in code.

~

EDIT (6/4/15): Please bear in mind that the sample code above is essentially a proof of concept to demonstrate the technique. Feel free to adapt as needed for your own specific application.

~

EDIT (7/24/15): It's gratifying that the above explanation seems to help demystify the issue. While I haven't tested it, the code by mohamede1945 [below] looks like a helpful optimization for practical purposes. Feel free to test it out and let us know what you think. (In the interest of completeness, I'll leave the sample code above as-is.)

Manning answered 1/2, 2015 at 21:34 Comment(22)
Apple takes a similar approach to redefining the width size class in Safari, so you can rest assured that this is a more or less supported approach. You shouldn't really need the extra ivars; UITraitCollection is optimized enough and overrideTraitCollectionForChildViewController is called rarely enough that it shouldn't be an issue to do the width check and create it then.Isa
@Isa Thanks. The sample code here is just to demonstrate the technique, and can obviously be optimized in various ways as desired. (As a general rule, with an object being used over & over repeatedly [e.g., whenever the device changes orientations], I don't think it's a bad idea to hold onto an object; but as you point out, the performance difference here may be minimal.)Manning
For my part, I don't have this sample code readily available in Swift, but it should be straightforward to translate.Manning
I´ve put together this solution, but the method overrideTraitCollectionForChildViewController only gets called when the app first starts and not when orientation changes. am I missing something?Bennet
@Bennet Have you properly set up your hierarchy as parent/child controllers? That's the first thing that comes to mind ...Manning
@Manning Is there a way to achieve the scenario mentioned in the link #29992002 . I was able to this using visual format. I would like to understand how to handle this purely using IBObstructionist
@Obstructionist Yes, I believe this is possible using the code above, as that's precisely the type of situation it was designed for. In answer to your question on the other page, I don't believe custom collection views by themselves would help, since the frames of those collection views within their Superview would still need to change. And I doubt Autolayout by itself would work (or would be difficult at best). Thus, being able to leverage Size Classes for iPad orientation seems like the best bet.Manning
@RonDiamond: Nice solution! But, I have a simple query. You have used wCompact|hRegular for iPad portrait view. But, that's supposed to be the size class for iPhone portrait. If someone is designing something in wCompact|hRegular class, will not it affect iPhone view?Creamer
@Rashmi: Thanks. Yes, you are correct. In this particular implementation example, the intent was for the iPad to mimic the iPhone layout in portrait mode. However, the specific choice of size class(es) is obviously up to you, to be overridden in whatever combination you see fit for your specific implementation.Manning
Cool!! Can you suggest me which size class I can use in my case. I am using same xib for iPhone portrait, iPad portrait and iPad landscape.Creamer
Also, any suggestions on how to package my existing view controller (where I am doing my interface layout in IB) as a child view controller?Creamer
@Rashmi: As I alluded to in the explanation, it ultimately doesn't matter which you choose -- you're just mapping the layout(s) to a pair of enum values. So you can really use whichever "size class" makes sense to you. I'd just make sure it doesn't conflict with some other (legitimate) size class that may be inherited somewhere as a default.Manning
As for the child view controller, I don't think it matters. You should be able to instantiate it programmatically or from a nib, as long as it's properly added as a child controller to the container.Manning
Amazing solution. However, Compact Width and Regular Height are traits for portrait version of 5.5inch devices. i.e. iPhone 6+. So if I have different constraints to be set to the portrait version of iPhone 6+ and a different layout for the portrait version for iPad this approach would not help me right? How do I achieve this?Zap
@uchiha Thanks for the good words. As for the particular choices of "size class," please see my comments to Rashmi, above.Manning
Is there any sample app by making use of this above code? If YES, Will appreciate if you share the same. ThanksTips
@Isa What evidence is there of Apple doing this in Safari? It seems to have the same layout in portrait as it does in landscape on the iPad.Stagger
Great explanation!! Is there any example having all these stuff implemented?Zap
This does work. The downside is that if you use external UIView subclass nibs inside the UIViewController nibs, they won't receive the overridden size changes. All your views have to be inside the iPad nib.Feliciafeliciano
working great! i spent sometime to achieve it without container but i think that does't work.Condensable
could you please take a look at this question #35915896. I'm having problems with displaying the size class correctlyTarsier
Note: Apple documentation says to always call super if you override -viewWillTransitionToSize:withTransitionCoordinator:Peaked
A
42

As a summary to the very long answer by RonDiamond. All you need to do is in your root view controller.

Objective-c

- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
    if (CGRectGetWidth(self.view.bounds) < CGRectGetHeight(self.view.bounds)) {
        return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
    } else {
        return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
    }
}

Swift:

override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection! {
        if view.bounds.width < view.bounds.height {
            return UITraitCollection(horizontalSizeClass: .Compact)
        } else {
            return UITraitCollection(horizontalSizeClass: .Regular)
        }
    }

Then in storyborad use compact width for Portrait and Regular width for Landscape.

Ancon answered 11/6, 2015 at 22:31 Comment(9)
I'm wondering how that's going to work out with the new split screen modes... one way to find out!Allare
UITraitCollection is only for iOS 8.0+Ancon
Doesn't work for me I have a view with two containerviews these need to be shown in a different location depending on the orientation. I created all the needed size classes it all works for iPhone. I tried using your code to get the iPad orientation separated as well. However it will just act weird on of the views. It doesn't change it like it's supposed to. Any clue what might be going on?Tarsier
Not able to help without looking at the code. Sorry.Ancon
I didn't get a notification you responded sorry. I did just create a question for it maybe it will give you the information you need.#35915896Tarsier
This worked for me when I overrode - (UITraitCollection *)traitCollection instead of overrideTraitCollectionForChildViewController. Also the constraints should match the traitcollections, so wC(hAny).Tutelage
This is the wrong approach, you need to make a parent view controller in which override willTransitionToTraitCollection and then setOverrideTraitCollection depending on newCollection, then call super.Bathsheb
@Bathsheb Can you elaborate on that with an answer?Cicerone
@Cicerone I would but it wouldn't answer the actual question. Take a look here for Apple's sample of how to use setOverrideTraitCollection github.com/ios8/AdaptivePhotosAnAdaptiveApplication/blob/master/…Bathsheb
L
4

The iPad has the 'regular' size trait for both horizontal and vertical dimensions, giving no distinction between portrait and landscape.

These size traits can be overridden in your custom UIViewController subclass code, via method traitCollection, for example:

- (UITraitCollection *)traitCollection {
    // Distinguish portrait and landscape size traits for iPad, similar to iPhone 7 Plus.
    // Be aware that `traitCollection` documentation advises against overriding it.
    UITraitCollection *superTraits = [super traitCollection];
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
        UITraitCollection *horizontalRegular = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
        UITraitCollection *verticalRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];
        UITraitCollection *regular = [UITraitCollection traitCollectionWithTraitsFromCollections:@[horizontalRegular, verticalRegular]];

        if ([superTraits containsTraitsInCollection:regular]) {
            if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation])) {
                // iPad in portrait orientation
                UITraitCollection *horizontalCompact = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
                return [UITraitCollection traitCollectionWithTraitsFromCollections:@[superTraits, horizontalCompact, verticalRegular]];
            } else {
                // iPad in landscape orientation
                UITraitCollection *verticalCompact = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassCompact];
                return [UITraitCollection traitCollectionWithTraitsFromCollections:@[superTraits, horizontalRegular, verticalCompact]];
            }
        }
    }
    return superTraits;
}

- (BOOL)prefersStatusBarHidden {
    // Override to negate this documented special case, and avoid erratic hiding of status bar in conjunction with `traitCollection` override:
    // For apps linked against iOS 8 or later, this method returns true if the view controller is in a vertically compact environment.
    return NO;
}

This gives the iPad the same size traits as the iPhone 7 Plus. Note that other iPhone models generally have the 'compact width' trait (rather than regular width) regardless of orientation.

Mimicking the iPhone 7 Plus in this way allows that model to be used as a stand-in for the iPad in Xcode's Interface Builder, which is unaware of customizations in code.

Be aware that Split View on the iPad may use different size traits from normal full screen operation.

This answer is based on the approach taken in this blog post, with some improvements.

Update 2019-01-02: Updated to fix intermittent hidden status bar in iPad landscape, and potential trampling of (newer) traits in UITraitCollection. Also noted that Apple documentation actually recommends against overriding traitCollection, so in future there may turn out to be issues with this technique.

Lacteous answered 14/12, 2016 at 15:10 Comment(1)
Apple specifies that the traitCollection property be read-only: developer.apple.com/documentation/uikit/uitraitenvironment/…Superorganic
C
4

The long and helpful answer by RonDiamond is a good start to comprehend the principles, however the code that worked for me (iOS 8+) is based on overriding method (UITraitCollection *)traitCollection

So, add constraints in InterfaceBuilder with variations for Width - Compact, for example for constraint's property Installed. So Width - Any will be valid for landscape, Width - Compact for Portrait.

To switch constraints in the code based on current view controller size, just add the following into your UIViewController class:

- (UITraitCollection *)traitCollection
{
    UITraitCollection *verticalRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];

    if (self.view.bounds.size.width < self.view.bounds.size.height) {
        // wCompact, hRegular
        return [UITraitCollection traitCollectionWithTraitsFromCollections:
                @[[UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact],
                  verticalRegular]];
    } else {
        // wRegular, hRegular
        return [UITraitCollection traitCollectionWithTraitsFromCollections:
                @[[UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular],
                  verticalRegular]];
    }
}
Curl answered 12/6, 2017 at 18:36 Comment(1)
The Apple documentation for traitCollection has the following warning: IMPORTANT: Don’t implement this property in your own objectsWinnebago
C
0

How much different is your landscape mode going to be than your portrait mode? If its very different, it might be a good idea to create another view controller and load it when the device is in landscape

For example

    if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) 
    //load landscape view controller here
Carve answered 29/10, 2014 at 15:4 Comment(3)
Yes this is one option. But I dont think its the most optimum one. My point is that if there is an option to use different sizing class traits for portrait and landscape modes for the iphone in ios8 then why not the same for the iPad?Ekaterinoslav
Before Xcode 6, we can use different storyboard for different orientation. That's not very efficient if most view controllers are the same. But it's very convenient for different layouts. In Xcode 6, there is no way to do so. Maybe creating different view controller for different orientation is the only solution.Ga
Loading different viewController each time seems very inefficient, especially if there is something happening on the screen. It's much better to use the same view and either manipulate autolayout constraint or position of the elements in code. Or to use the hack mentioned above, until apple addresses this issue.Gnathion
M
0

Swift 5 Version. It works fine.

override func overrideTraitCollection(forChild childViewController: UIViewController) -> UITraitCollection? {
    if UIScreen.main.bounds.width > UIScreen.main.bounds.height {
        let collections = [UITraitCollection(horizontalSizeClass: .regular),
                           UITraitCollection(verticalSizeClass: .compact)]
        return UITraitCollection(traitsFrom: collections)
    }
    return super.overrideTraitCollection(forChild: childViewController)
}
Moderato answered 26/2, 2020 at 7:58 Comment(0)
M
-3

Swift 3.0 code for @RonDiamond solution

class Test : UIViewController {


var _willTransitionToPortrait: Bool?
var _traitCollection_CompactRegular: UITraitCollection?
var _traitCollection_AnyAny: UITraitCollection?

func viewDidLoad() {
    super.viewDidLoad()
    self.upReferenceSizeClasses = null
}

func setUpReferenceSizeClasses() {
    var traitCollection_hCompact: UITraitCollection = UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClassCompact)
    var traitCollection_vRegular: UITraitCollection = UITraitCollection(verticalSizeClass: UIUserInterfaceSizeClassRegular)
    _traitCollection_CompactRegular = UITraitCollection(traitsFromCollections: [traitCollection_hCompact,traitCollection_vRegular])
    var traitCollection_hAny: UITraitCollection = UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClassUnspecified)
    var traitCollection_vAny: UITraitCollection = UITraitCollection(verticalSizeClass: UIUserInterfaceSizeClassUnspecified)
    _traitCollection_AnyAny = UITraitCollection(traitsFromCollections: [traitCollection_hAny,traitCollection_vAny])
}

func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    _willTransitionToPortrait = self.view.frame.size.height > self.view.frame.size.width
}

func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    _willTransitionToPortrait = size.height > size.width
}

func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection {
    var traitCollectionForOverride: UITraitCollection = _willTransitionToPortrait ? _traitCollection_CompactRegular : _traitCollection_AnyAny
    return traitCollectionForOverride
}}
Mcilroy answered 8/5, 2017 at 11:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.