14
votes

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?

6
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
@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
@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

6 Answers

7
votes

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.

5
votes

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
5
votes

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.

3
votes

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
2
votes

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

"

1
votes

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