Sizing class for iPad portrait and Landscape Modes

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?

  • Anonymously Log In to an App with iCloud Apple ID
  • Disable UITextField Predictive Text
  • Change UIBarButtonItem from UISearchBar
  • hidesBarsOnSwipe never shows navbar again when scrolling up
  • Set notifications from Today Extension Widget
  • What am I doing wrong in Swift for calling this Objective-C block/API call?
  • 6 Solutions Collect From Internet About “Sizing class for iPad portrait and Landscape Modes”

    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 {
        _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.)

    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.

    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.
        UITraitCollection *superTraits = [super traitCollection];
        if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
            UITraitCollection *horizontalRegular = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
            UITraitCollection *horizontalCompact = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
            UITraitCollection *verticalRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];
            UITraitCollection *verticalCompact = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassCompact];
    
            UITraitCollection *regular = [UITraitCollection traitCollectionWithTraitsFromCollections:@[horizontalRegular, verticalRegular]];
            UITraitCollection *portrait = [UITraitCollection traitCollectionWithTraitsFromCollections:@[horizontalCompact, verticalRegular]];
            UITraitCollection *landscape = [UITraitCollection traitCollectionWithTraitsFromCollections:@[horizontalRegular, verticalCompact]];
    
            if ([superTraits containsTraitsInCollection:regular]) {
                if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation]))
                    return portrait;
                else
                    return landscape;
            }
        }
        return superTraits;
    }
    

    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.

    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]];
        }
    }
    

    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
    

    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
    }}