15

I have an app targeted iOS8 and initial view controller is UISplitViewController. I use storyboard, so that it kindly instantiate everything for me.

Because of my design I need SplitViewController to show both master and detail views in portrait mode on iPhone. So I am looking for a way to override trait collection for this UISplitViewController.

I found that I can use

 override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator!) { ... }

but, unfortunately, there are only methods to override child controllers traits collections:

setOverrideTraitCollection(collection: UITraitCollection!, forChildViewController childViewController: UIViewController!)

and I can't do so for self in my UISplitViewController subclass.

I checked an example app Adaptive Photos from Apple. And in this app author use special TraitOverrideViewController as root and some magic in his viewController setter to make it all works.

It looks horrible for me. Is there are any way around to override traits? Or If there are not, how can I manage to use the same hack with storyboard? In other words, how to inject some viewController as root one only to handle traits for my UISplitViewController with storyboard?

Ilya Belikin
  • 645
  • 1
  • 5
  • 15
  • Why does it look horrible? `AAPLTraitOverrideViewController.m` is barely more than 20 lines of code. It should take you maybe 10 minutes to translate that into Swift and then you don't have to look at it again. – Mundi Aug 25 '14 at 11:21
  • @Mundi see update. And no, I don't believe this kind of code will be free to maintain. At last now I need to explain my colleges what is it and why we need one. I can't reason about each line, which makes me feel bad. Why we need didMoveToParentViewController? Or why we need shouldAutomaticallyForwardRotationMethods if it is deprecated? – Ilya Belikin Aug 25 '14 at 12:01
  • @Mundi and example app did not check traits on load, so it will be in wrong condition if you will launch it in portrait... my code above inherit this issue. Will update it. – Ilya Belikin Aug 25 '14 at 14:10

6 Answers6

8

Ok, I wish there was another way around this, but for now I just converted code from the Apple example to Swift and adjusted it to use with Storyboards.

It works, but I still believe it is an awful way to archive this goal.

My TraitOverride.swift:

import UIKit

class TraitOverride: UIViewController {

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    var forcedTraitCollection: UITraitCollection? {
        didSet {
            updateForcedTraitCollection()
        }
    }

    override func viewDidLoad() {
        setForcedTraitForSize(view.bounds.size)
    }

    var viewController: UIViewController? {
        willSet {
            if let previousVC = viewController {
                if newValue !== previousVC {
                    previousVC.willMoveToParentViewController(nil)
                    setOverrideTraitCollection(nil, forChildViewController: previousVC)
                    previousVC.view.removeFromSuperview()
                    previousVC.removeFromParentViewController()
                }
            }
        }

        didSet {
            if let vc = viewController {
                addChildViewController(vc)
                view.addSubview(vc.view)
                vc.didMoveToParentViewController(self)
                updateForcedTraitCollection()
            }
        }
    }

    override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator!) {
        setForcedTraitForSize(size)
        super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
    }

    func setForcedTraitForSize (size: CGSize) {

        let device = traitCollection.userInterfaceIdiom
        var portrait: Bool {
            if device == .Phone {
                return size.width > 320
            } else {
                return size.width > 768
            }
        }

        switch (device, portrait) {
        case (.Phone, true):
            forcedTraitCollection = UITraitCollection(horizontalSizeClass: .Regular)
        case (.Pad, false):
            forcedTraitCollection = UITraitCollection(horizontalSizeClass: .Compact)
        default:
            forcedTraitCollection = nil
        }
    }

    func updateForcedTraitCollection() {
        if let vc = viewController {
            setOverrideTraitCollection(self.forcedTraitCollection, forChildViewController: vc)
        }
    }

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        performSegueWithIdentifier("toSplitVC", sender: self)
    }

    override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
        if segue.identifier == "toSplitVC" {
            let destinationVC = segue.destinationViewController as UIViewController
            viewController = destinationVC
        }
    }

    override func shouldAutomaticallyForwardAppearanceMethods() -> Bool {
        return true
    }

    override func shouldAutomaticallyForwardRotationMethods() -> Bool {
        return true
    }
}

To make it work you need to add a new UIViewController on the storyboard and made it the initial. Add show segue from it to your real controller like this: storyboard

You need to name the segue "toSplitVC": segue name

and set initial controller to be TraitOverride: assign controller

Now it should work for you too. Let me know if you find a better way or any flaws in this one.

Brandon A
  • 8,153
  • 3
  • 42
  • 77
Ilya Belikin
  • 645
  • 1
  • 5
  • 15
  • Be cautious when you use this code. For me it crashes in updateForcedTraitCollection. – IvanMih Sep 15 '19 at 07:11
  • if you present inside a navigation controller you need to call self.navigationController!.setOverrideTraitCollection(self.forcedTraitCollection, forChild: vc) – user1760527 Jun 13 '20 at 14:45
5

I understand that you wanted a SWIFT translation here... And you've probably solved that.

Below is something I've spent a considerable time trying to resolve - getting my SplitView to work on an iPhone 6+ - this is a Cocoa solution.

My Application is TabBar based and the SplitView has Navigation Controllers. In the end my issue was that setOverrideTraitCollection was not being sent to the correct target.

@interface myUITabBarController ()

@property (nonatomic, retain) UITraitCollection *overrideTraitCollection;

@end

@implementation myUITabBarController

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self performTraitCollectionOverrideForSize:self.view.bounds.size];
}

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
    NSLog(@"myUITabBarController %@", NSStringFromSelector(_cmd));
    [self performTraitCollectionOverrideForSize:size];

    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
}

- (void)performTraitCollectionOverrideForSize:(CGSize)size
{
    NSLog(@"myUITabBarController %@", NSStringFromSelector(_cmd));

    _overrideTraitCollection = nil;

    if (size.width > 320.0)
    {
        _overrideTraitCollection = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
    }

    [self setOverrideTraitCollection:_overrideTraitCollection forChildViewController:self];

    for (UIViewController * view in self.childViewControllers)
    {
        [self setOverrideTraitCollection:_overrideTraitCollection forChildViewController:view];
        NSLog(@"myUITabBarController %@ AFTER  viewTrait=%@", NSStringFromSelector(_cmd), [view traitCollection]);
    }
}

@end
David Wilson
  • 128
  • 2
  • 7
  • Just to let you know I get a graphical glitch when going from portrait to landscape on the 6 plus with this code. – malhal Jan 28 '17 at 21:36
  • And when you hide the tab bar, navigation controllers down the line have a misplaced toolbar, floats above where the tab bar would have been. – malhal Jan 29 '17 at 13:53
5

UPDATE:

Apple do not recommend doing this:

Use the traitCollection property directly. Do not override it. Do not provide a custom implementation.

I'm not overriding this property anymore! Now I'm calling overrideTraitCollectionForChildViewController: in the parent viewControler class.

Old answer:

I know it's more than a year since question was asked, but i think my answer will help someone like me who do not achieved success with the accepted answer.

Whell the solution is really simple, you can just override traitCollection: method. Here is an example from my app:

- (UITraitCollection *)traitCollection {
    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
        return super.traitCollection;
    } else {
        switch (self.modalPresentationStyle) {
            case UIModalPresentationFormSheet:
            case UIModalPresentationPopover:
                return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];

            default:
                return super.traitCollection;
        }
    }
}

the idea is to force Compact size class on iPad if controller is presented as popover or form sheet.

Hope it helps.

malhal
  • 26,330
  • 7
  • 115
  • 133
Oleg_Korchickiy
  • 298
  • 5
  • 15
  • This helped a lot ! thank you, but actually if presented form sheet iPad controller returns a Compact Size class ! It was helpfull to me cause I forced a UIUserInterfaceSizeClassRegular Class instead ! – Michael Pirotte Feb 05 '16 at 10:49
  • FYI, here is the link for that note about not overriding the `traitCollection`: https://developer.apple.com/reference/uikit/uitraitenvironment/1623514-traitcollection – Rob Mar 26 '17 at 19:07
3

The extra top level VC works well for a simple app but it won't propagate down to modally presented VC's as they don't have a parentVC. So you need to insert it again in different places.

A better approach I found was just to subclass UINavigationController and then just use your subclass in the storyboard and elsewhere where you would normally use UINavigationController. It saves the additional VC clutter in storyboards and also saves extra clutter in code.

This example will make all iPhones use regular horizontal size class for landscape.

@implementation MyNavigationController

- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
    UIDevice *device = [UIDevice currentDevice];

    if (device.userInterfaceIdiom == UIUserInterfaceIdiomPhone && CGRectGetWidth(childViewController.view.bounds) > CGRectGetHeight(childViewController.view.bounds)) {
        return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
    }

    return nil;
}

@end
trapper
  • 11,716
  • 7
  • 38
  • 82
  • this method shouldn't be overridden – malhal Jan 28 '17 at 21:23
  • Because its called a million times, plus its too late to affect that controller. – malhal Jan 31 '17 at 02:04
  • 1
    It does work, I use this code in my own app. If it is documented to not override this method then I am interested to see where. – trapper Jan 31 '17 at 02:09
  • it maybe works for your apps because navigation controller isn't affected by traits like split view controller is. – malhal Jan 31 '17 at 16:19
  • @malhal I think you're mistaken. You are not supposed to override [traitCollection](https://developer.apple.com/documentation/uikit/uitraitenvironment/1623514-traitcollection). There is nothing in the [docs](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621486-overridetraitcollection) about not overriding `overrideTraitCollection(forChildViewController childViewController: UIViewController) -> UITraitCollection?` – DoesData Apr 22 '18 at 22:07
  • In this doc it says “use this method” it does not say override this method: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621406-setoverridetraitcollection?language=objc – malhal Apr 22 '18 at 22:28
2

Yes, it must use custom container View Controller to override the function viewWillTransitionToSize. You use the storyboard to set the container View Controller as initial. Also, you can refer this good artical which use the program to implement it. According to it, your judgement portait could have some limitations:

var portrait: Bool {
    if device == .Phone {
       return size.width > 320
    } else {
       return size.width > 768
    }
}

other than

    if **size.width > size.height**{
        self.setOverrideTraitCollection(UITraitCollection(horizontalSizeClass: UIUserInterfaceSizeClass.Regular), forChildViewController: viewController)
    }
    else{
        self.setOverrideTraitCollection(nil, forChildViewController: viewController)
    }

"

Jenus Dong
  • 304
  • 2
  • 7
1

Props To @Ilyca

Swift 3

import UIKit

class TraitOverride: UIViewController {

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)!
    }

    var forcedTraitCollection: UITraitCollection? {
        didSet {
            updateForcedTraitCollection()
        }
    }

    override func viewDidLoad() {
        setForcedTraitForSize(size: view.bounds.size)
    }

    var viewController: UIViewController? {
        willSet {
            if let previousVC = viewController {
                if newValue !== previousVC {
                    previousVC.willMove(toParentViewController: nil)
                    setOverrideTraitCollection(nil, forChildViewController: previousVC)
                    previousVC.view.removeFromSuperview()
                    previousVC.removeFromParentViewController()
                }
            }
        }

        didSet {
            if let vc = viewController {
                addChildViewController(vc)
                view.addSubview(vc.view)
                vc.didMove(toParentViewController: self)
                updateForcedTraitCollection()
            }
        }
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        setForcedTraitForSize(size: size)
        super.viewWillTransition(to: size, with: coordinator)
    }

    func setForcedTraitForSize (size: CGSize) {

        let device = traitCollection.userInterfaceIdiom
        var portrait: Bool {
            if device == .phone {
                return size.width > 320
            } else {
                return size.width > 768
            }
        }

        switch (device, portrait) {
        case (.phone, true):
            forcedTraitCollection = UITraitCollection(horizontalSizeClass: .regular)
        case (.pad, false):
            forcedTraitCollection = UITraitCollection(horizontalSizeClass: .compact)
        default:
            forcedTraitCollection = nil
        }
    }

    func updateForcedTraitCollection() {
        if let vc = viewController {
            setOverrideTraitCollection(self.forcedTraitCollection, forChildViewController: vc)
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        performSegue(withIdentifier: "toSplitVC", sender: self)
    }

    override var shouldAutomaticallyForwardAppearanceMethods: Bool {
        return true
    }

    override func shouldAutomaticallyForwardRotationMethods() -> Bool {
        return true
    }
}
Brandon A
  • 8,153
  • 3
  • 42
  • 77
  • Thanks for this translation to Swift 3. To make it work, I had to add `override func prepare(for segue: UIStoryboardSegue, sender: Any?)` like in the Swift 2 code above (with a few modifications). – eli Jun 21 '17 at 12:05
  • Sure thing. Props to @Ilyca for introducing this to me as well. – Brandon A Jun 21 '17 at 16:58