How to override trait collection for initial UIViewController? (with Storyboard)
Asked Answered
B

7

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?

Bravura answered 25/8, 2014 at 10:1 Comment(3)
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.Mercury
@Mercury 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?Bravura
@Mercury 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.Bravura
B
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.

Bravura answered 29/8, 2014 at 9:50 Comment(2)
Be cautious when you use this code. For me it crashes in updateForcedTraitCollection.Stator
if you present inside a navigation controller you need to call self.navigationController!.setOverrideTraitCollection(self.forcedTraitCollection, forChild: vc)Evolutionary
S
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
Savonarola answered 11/1, 2015 at 1:46 Comment(2)
Just to let you know I get a graphical glitch when going from portrait to landscape on the 6 plus with this code.Georgianngeorgianna
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.Georgianngeorgianna
A
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.

Atombomb answered 20/1, 2016 at 19:46 Comment(2)
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 !Intractable
FYI, here is the link for that note about not overriding the traitCollection: developer.apple.com/reference/uikit/uitraitenvironment/…Embosser
T
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
Trail answered 24/11, 2016 at 1:15 Comment(6)
this method shouldn't be overriddenGeorgianngeorgianna
Because its called a million times, plus its too late to affect that controller.Georgianngeorgianna
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.Trail
it maybe works for your apps because navigation controller isn't affected by traits like split view controller is.Georgianngeorgianna
@Georgianngeorgianna I think you're mistaken. You are not supposed to override traitCollection. There is nothing in the docs about not overriding overrideTraitCollection(forChildViewController childViewController: UIViewController) -> UITraitCollection?Estivation
In this doc it says “use this method” it does not say override this method: developer.apple.com/documentation/uikit/uiviewcontroller/…Georgianngeorgianna
F
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)
    }

"

Foreknowledge answered 2/12, 2014 at 10:21 Comment(0)
D
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
    }
}
Dispart answered 25/4, 2017 at 0:20 Comment(2)
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).Gamine
Sure thing. Props to @Ilyca for introducing this to me as well.Dispart
K
1

When using iOS 17 or newer, you can directly override trait properties by setting the traitOverrides property of the view controller. Example:

override func viewDidLoad() {
    super.viewDidLoad()
    
    traitOverrides.horizontalSizeClass = .compact
}
Kindly answered 11/6 at 13:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.