如何在故事板中重写 trait collection 的初始 UIViewController?

15

我有一个面向iOS8的应用程序,初始视图控制器是UISplitViewController。我使用故事板,以便它可以为我实例化所有内容。

由于我的设计,我需要在iPhone的纵向模式下显示SplitViewController的主视图和详细视图。因此,我正在寻找一种方法来覆盖此UISplitViewController的trait collection。

我发现我可以使用

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

但不幸的是,只有覆盖子控制器特征集合的方法:

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

我无法在我的UISplitViewController子类中为自己执行此操作。

我查看了一个来自苹果的示例应用程序适应性照片。在这个应用程序中,作者使用特殊的TraitOverrideViewController作为根视图控制器,并在他的视图控制器setter中使用了一些魔法来使其所有工作正常。

对我来说,这看起来很可怕。有没有绕过覆盖特征的方法?如果没有,我该如何使用故事板来使用相同的技巧呢?换句话说,如何仅注入某些视图控制器作为根视图处理我的UISplitViewController与故事板的特征?


为什么看起来这么糟糕?AAPLTraitOverrideViewController.m代码只有20行左右。你可能只需要花费10分钟将其转换为Swift,然后就不必再去看它了。 - Mundi
@Mundi 看一下更新。不,我不相信这种代码会免费维护。最后现在我需要向我的同事解释它是什么以及为什么我们需要一个。我无法理解每一行代码,这让我感到很糟糕。为什么我们需要didMoveToParentViewController?或者为什么我们需要shouldAutomaticallyForwardRotationMethods如果它已经被弃用了? - Ilya Belikin
@Mundi和示例应用在加载时没有检查特征,因此如果您在纵向模式下启动它,它将处于错误状态...我上面的代码也继承了这个问题。将对其进行更新。 - Ilya Belikin
6个回答

8

好的,我希望有另一种方法来解决这个问题,但现在我只是将苹果示例代码转换为Swift并进行了调整,以便与Storyboard一起使用。

它确实有效,但我仍然认为这是一个可怕的实现目标的方法。

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

为了使其工作,您需要在故事板上添加一个新的UIViewController并将其设置为初始控制器。像这样添加show segue到您的真实控制器: storyboard 您需要将segue命名为“toSplitVC”: segue name 并将初始控制器设置为TraitOverride: assign controller 现在它应该也适用于您。如果您发现更好的方法或此方法中有任何缺陷,请告诉我。

在使用这段代码时要小心。对我来说,在 updateForcedTraitCollection 函数中会导致崩溃。 - IvanMih
如果您在导航控制器中呈现,则需要调用self.navigationController!.setOverrideTraitCollection(self.forcedTraitCollection, forChild: vc)。 - user1760527

5

我知道您想在这里进行SWIFT翻译... 您可能已经解决了这个问题。

以下是我花费相当长时间尝试解决的问题 - 在iPhone 6+上使我的SplitView工作 - 这是一个Cocoa解决方案。

我的应用程序基于TabBar,SplitView具有导航控制器。最终我的问题是setOverrideTraitCollection未发送到正确的目标。

@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

只是想让您知道,使用此代码在6 Plus上从纵向切换到横向时会出现图形故障。 - malhal
当您隐藏选项卡栏时,后续的导航控制器会出现工具栏错位的问题,浮动在选项卡栏原本所在的位置上方。 - malhal

5

更新:

苹果不建议这样做:

直接使用traitCollection属性。不要覆盖它。不提供自定义实现。

我不再覆盖此属性! 现在我在父视图控制器类中调用overrideTraitCollectionForChildViewController:

旧答案:

我知道问题被提出已经超过一年,但我认为我的答案会帮助像我一样没有成功的人。

解决方案非常简单,您只需覆盖traitCollection:方法。以下是我的应用程序示例:

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

如果控制器以弹出窗口或表单表格的形式呈现,那么将在iPad上强制使用紧凑型大小类。

希望这能有所帮助。


这非常有帮助!谢谢你,但实际上,如果以表单表现形式呈现,iPad控制器会返回一个紧凑的尺寸类!这对我很有帮助,因为我强制使用了UIUserInterfaceSizeClassRegular类! - Michael Pirotte
这是关于不要覆盖 traitCollection 的笔记的链接:https://developer.apple.com/reference/uikit/uitraitenvironment/1623514-traitcollection。 - Rob

3

额外的顶层VC适用于简单的应用程序,但它不会向下传播到模态呈现的VC,因为它们没有父VC。因此,您需要在不同的位置再次插入它。

我发现更好的方法是子类化UINavigationController,然后只在故事板和其他通常使用UINavigationController的地方使用您的子类。这样可以节省故事板中的额外VC混乱,也可以节省代码中的额外混乱。

此示例将使所有iPhone在横向方向上使用常规水平大小类。

@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

此方法不应被覆盖。 - malhal
因为它被调用了无数次,而且现在改变也已经太晚了,不会影响那个控制器。 - malhal
1
它确实有效,我在自己的应用程序中使用了这段代码。如果文档中没有说明不要覆盖此方法,那么我很想知道在哪里可以找到相关信息。 - trapper
这可能适用于您的应用程序,因为导航控制器不受像分割视图控制器那样的特征影响。 - malhal
@malhal,我认为你错了。你不能重写traitCollection。在文档中没有关于不重写overrideTraitCollection(forChildViewController childViewController: UIViewController) -> UITraitCollection?的内容。 - DoesData
在这份文档中,它说“使用此方法”,而不是覆盖此方法:https://developer.apple.com/documentation/uikit/uiviewcontroller/1621406-setoverridetraitcollection?language=objc - malhal

2
是的,必须使用自定义容器视图控制器来覆盖函数viewWillTransitionToSize。您可以使用故事板将容器视图控制器设置为初始状态。 此外,您可以参考this good artical,该文章使用程序实现它。根据它,您的判断肖像可能会有一些限制:
var portrait: Bool {
    if device == .Phone {
       return size.width > 320
    } else {
       return size.width > 768
    }
}

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

"


1

致敬@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
    }
}

感谢将此代码转换为Swift 3。为了让它工作,我必须添加override func prepare(for segue: UIStoryboardSegue, sender: Any?),就像上面的Swift 2代码一样(稍作修改即可)。 - eli
当然没问题。也要感谢@Ilyca向我介绍这个。 - Brandon A

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接