iPad竖屏和横屏模式的尺寸分类

100

我希望使用在xcode 6中引入的Sizing Classes,根据iPad(纵向或横向)的方向,以不同的位置放置我的子视图。我找到了许多教程,解释了IB中为iPhone提供不同大小类别的纵向和横向,但是似乎没有覆盖IB上iPad单独的纵向或横向模式的教程。有人能帮忙吗?


似乎没有非编程解决方案可用,这对于默认屏幕是必需的。 - SwiftArchitect
7个回答

176

苹果似乎想将iPad的两个方向视为相同的 - 但正如我们中的许多人发现的那样,有非常合理的设计原因希望在iPad纵向和iPad横向上改变UI布局。

不幸的是,当前的操作系统似乎没有提供支持这种区别的功能...这意味着我们需要通过编写代码来操作自动布局约束或类似的解决方法来实现理想情况下可以免费使用自适应 UI 实现的东西。

这不是一种优雅的解决方案。

难道没有办法利用苹果已经内置到IB和UIKit中的神奇功能,以使用我们选择的大小类来处理给定方向的布局吗?

在更泛化地考虑问题时,我意识到“大小类”只是一种方式,用于处理存储在IB中的多个布局,以便可以在运行时调用它们。

实际上,“大小类”只是一对枚举值。从UIInterface.h:

typedef NS_ENUM(NSInteger, UIUserInterfaceSizeClass) {
    UIUserInterfaceSizeClassUnspecified = 0,
    UIUserInterfaceSizeClassCompact     = 1,
    UIUserInterfaceSizeClassRegular     = 2,
} NS_ENUM_AVAILABLE_IOS(8_0);

所以不管苹果决定如何给这些不同的变量命名,从根本上说,它们只是一对用作某种唯一标识符的整数,用于区分一个布局与另一个布局,在IB中存储。

假设我们在IB中创建一个备用布局(使用未使用的尺寸类别),比如iPad纵向模式...有没有办法让设备在运行时使用我们选择的尺寸类别(UI布局)?

在尝试了几种不太优雅的方法后,我怀疑可能有一种方式可以在编程时覆盖默认的尺寸类别。确实有(在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);

因此,如果您可以将您的视图控制器层次结构打包为“子”视图控制器,并将其添加到顶级父视图控制器中...那么您可以有条件地覆盖该子视图控制器,使其认为它的大小类别与操作系统的默认值不同。

以下是在“父”视图控制器中执行此操作的示例实现:

@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 {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]
    _willTransitionToPortrait = size.height > size.width;
}

-(UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController {
    UITraitCollection *traitCollectionForOverride = _willTransitionToPortrait ? _traitCollection_CompactRegular : _traitCollection_AnyAny;
    return traitCollectionForOverride;
}
@end

为了快速演示它是否有效,我在IB中特别为子控制器布局的'Regular / Regular'和'Compact / Regular'版本添加了自定义标签:

enter image description here enter image description here

以下是运行时的外观,在iPad处于两个方向时: enter image description here enter image description here

看,运行时的自定义大小类配置。

希望苹果公司能在下一个操作系统版本中消除这种需求。与通过编程方式干预自动布局约束或进行其他代码操纵相比,这可能是一种更优雅、可扩展的方法。

~

编辑(6/4/15):请注意,上面的示例代码本质上是一种证明技术的方法。请根据自己的具体应用程序进行自适应。

~

编辑(7/24/15):令人满意的是,上述解释似乎有助于揭开神秘面纱。虽然我没有测试过它,但mohamede1945 [下面] 的代码看起来是一个有用的优化。请随时测试并告诉我们您的想法。(为了完整起见,我将保留上述示例代码。)


1
@RonDiamond,好文! - amergin
3
苹果在重新定义Safari中的宽度大小类方面采用了类似的方法,因此您可以放心,这是一种更受支持的方法。您实际上不需要额外的ivars; UITraitCollection已经足够优化,并且overrideTraitCollectionForChildViewController调用的频率很少,所以检查宽度并在那时创建它不应该是一个问题。 - zwaldowski
1
@zwaldowski 谢谢。这里的示例代码只是为了演示技术,显然可以根据需要以各种方式进行优化。(通常情况下,如果一个对象被反复使用[例如,每当设备改变方向时],我认为保留该对象并不是一个坏主意;但正如你所指出的,这里的性能差异可能很小。) - RonDiamond
1
@Rashmi:正如我在解释中所提到的,最终选择哪个并不重要——你只是将布局映射到一对枚举值。因此,你可以使用任何对你有意义的“大小类”。我只是确保它不会与其他(合法的)大小类冲突,这些大小类可能作为默认值在某个地方被继承。 - RonDiamond
1
关于子视图控制器,我认为这并不重要。只要它被正确地添加为容器的子控制器,你就可以通过编程或从nib实例化它。 - RonDiamond
显示剩余18条评论

41

作为对RonDiamond非常冗长回答的总结,你所需要做的就是在你的根视图控制器中。

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

然后在故事板中,使用紧凑宽度来设置纵向模式,使用常规宽度来设置横向模式。


我在想新的分屏模式会如何运作……只有一种方法,去试试! - mm2001
对我来说不起作用。我有一个视图,其中包含两个容器视图,这些视图需要根据方向在不同的位置显示。我创建了所有需要的尺寸类别,对于iPhone都可以正常工作。我尝试使用你的代码将iPad的方向也分开处理。然而,其中一个视图的行为就变得奇怪了。它没有像预期的那样进行更改。你有什么线索可能是怎么回事吗? - NoSixties
1
当我重写 - (UITraitCollection *)traitCollection 而不是 overrideTraitCollectionForChildViewController 时,这对我有用。此外,约束应该匹配 traitcollections,因此wC(hAny)。 - H. de Jonge
这是错误的方法,你需要创建一个父视图控制器,在其中重写willTransitionToTraitCollection方法,然后根据newCollection设置setOverrideTraitCollection,最后调用super。 - malhal
1
@Apollo 我可以,但这并不能回答实际问题。请看这里,了解苹果如何使用setOverrideTraitCollection的示例:https://github.com/ios8/AdaptivePhotosAnAdaptiveApplication/blob/master/AdaptivePhotos/AAPLTraitOverrideViewController.m - malhal
显示剩余4条评论

4
RonDiamond提供的详细且有用的答案是理解原则的好开端,但对我而言(iOS 8+),可行的代码是基于覆盖方法(UITraitCollection *)traitCollection
因此,在InterfaceBuilder中添加具有宽度变化的约束-紧凑型,例如对于约束属性Installed。所以Width - Any将适用于横屏,Width - Compact将适用于竖屏。
为了根据当前视图控制器大小切换约束,只需在UIViewController类中添加以下内容:
- (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]];
    }
}

traitCollection的苹果文档中有以下警告:重要提示:不要在您自己的对象中实现此属性 - undefined

4

iPad在水平和垂直尺寸上都有“常规”大小特征,没有肖像和风景之分。

这些大小特征可以通过您的自定义UIViewController子类代码中的traitCollection方法进行覆盖,例如:

- (UITraitCollection *)traitCollection {
    // Distinguish portrait and landscape size traits for iPad, similar to iPhone 7 Plus.
    // Be aware that `traitCollection` documentation advises against overriding it.
    UITraitCollection *superTraits = [super traitCollection];
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
        UITraitCollection *horizontalRegular = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
        UITraitCollection *verticalRegular = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassRegular];
        UITraitCollection *regular = [UITraitCollection traitCollectionWithTraitsFromCollections:@[horizontalRegular, verticalRegular]];

        if ([superTraits containsTraitsInCollection:regular]) {
            if (UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation])) {
                // iPad in portrait orientation
                UITraitCollection *horizontalCompact = [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
                return [UITraitCollection traitCollectionWithTraitsFromCollections:@[superTraits, horizontalCompact, verticalRegular]];
            } else {
                // iPad in landscape orientation
                UITraitCollection *verticalCompact = [UITraitCollection traitCollectionWithVerticalSizeClass:UIUserInterfaceSizeClassCompact];
                return [UITraitCollection traitCollectionWithTraitsFromCollections:@[superTraits, horizontalRegular, verticalCompact]];
            }
        }
    }
    return superTraits;
}

- (BOOL)prefersStatusBarHidden {
    // Override to negate this documented special case, and avoid erratic hiding of status bar in conjunction with `traitCollection` override:
    // For apps linked against iOS 8 or later, this method returns true if the view controller is in a vertically compact environment.
    return NO;
}

这样做使iPad具有与iPhone 7 Plus相同的大小特征。请注意,其他iPhone型号通常具有“紧凑宽度”特征(而非常规宽度),无论方向如何。
通过这种方式模仿iPhone 7 Plus允许该型号在Xcode的界面生成器中被用作iPad的替身,该生成器不知道代码中的自定义内容。
请注意,iPad上的Split View可能会使用与正常全屏操作不同的大小特征。
本答案基于这篇博客文章采取的方法,并进行了一些改进。
更新2019-01-02:更新以修复iPad横向时间歇性隐藏状态栏和UITraitCollection中可能被践踏的(更高版本的)特征。\n还指出苹果文档实际上建议不要覆盖traitCollection,因此在将来可能会出现该技术的问题。

苹果公司规定traitCollection属性为只读:https://developer.apple.com/documentation/uikit/uitraitenvironment/1623514-traitcollection - cleverbit

0

你的横屏模式和竖屏模式有多大的不同?如果非常不同,那么创建另一个视图控制器并在设备处于横屏时加载它可能是个好主意。

例如:

    if (UIDeviceOrientationIsLandscape([UIDevice currentDevice].orientation)) 
    //load landscape view controller here

是的,这是一个选项。但我认为它不是最优选项。我的观点是,如果在iOS 8中iPhone有使用不同的大小类特性来支持纵向和横向模式的选项,为什么iPad不能使用相同的呢? - neelIVP
在 Xcode 6之前,我们可以为不同的方向使用不同的故事板。但是,如果大多数视图控制器相同,那么这并不是非常高效的。但是,对于不同的布局来说非常方便。在Xcode 6中,没有其他方法可以做到这一点。也许为不同的方向创建不同的视图控制器是唯一的解决方案。 - Bagusflyer
2
每次加载不同的视图控制器似乎非常低效,特别是如果屏幕上有一些事情正在发生。最好使用相同的视图,并在代码中操作自动布局约束或元素的位置。或者使用上面提到的hack,直到苹果解决这个问题。 - Pahnev

0

Swift 5版本。它工作正常。

override func overrideTraitCollection(forChild childViewController: UIViewController) -> UITraitCollection? {
    if UIScreen.main.bounds.width > UIScreen.main.bounds.height {
        let collections = [UITraitCollection(horizontalSizeClass: .regular),
                           UITraitCollection(verticalSizeClass: .compact)]
        return UITraitCollection(traitsFrom: collections)
    }
    return super.overrideTraitCollection(forChild: childViewController)
}

-3

Swift 3.0代码,用于@RonDiamond方案

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

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