我可以使用自动布局来为横向和纵向方向提供不同的约束条件吗?

50

设备旋转时更改约束是否可能?如何实现?

一个简单的例子可能是两个图像,在竖屏模式下,一个在另一个上方堆叠,但在横屏模式下则并排。

如果这不可能,我该如何实现此布局?

我正在使用代码构建我的视图和约束,而不使用界面构建器。

6个回答

41

编辑:使用Xcode 6中引入的尺寸类别的新概念,您可以轻松地在Interface Builder中为特定的尺寸类别设置不同的约束条件。大多数设备(例如所有当前的iPhone)在横向模式下都有一个紧凑的垂直尺寸类别。

这比确定设备的方向更好的一般布局决策概念。

话虽如此,如果您真的需要知道方向,则可以使用UIDevice.currentDevice().orientation


原始帖子:

重写UIViewControllerupdateViewConstraints方法,为特定情况提供布局约束条件。这样,根据情况,布局总是正确设置的。确保它们与Storyboard中创建的那些形成完整的约束集合。您可以使用IB设置常规约束,并标记那些在运行时需要删除的约束。

我使用以下实现方式来为每个方向呈现不同的约束集合:

-(void)updateViewConstraints {
    [super updateViewConstraints];

    // constraints for portrait orientation
    // use a property to change a constraint's constant and/or create constraints programmatically, e.g.:
    if (!self.layoutConstraintsPortrait) {
        UIView *image1 = self.image1;
        UIView *image2 = self.image2;
        self.layoutConstraintsPortrait = [[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[image1]-[image2]-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(image1, image2)] mutableCopy];
        [self.layoutConstraintsPortrait addObject:[NSLayoutConstraint constraintWithItem:image1 attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem: image1.superview attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
        [self.layoutConstraintsPortrait addObject:[NSLayoutConstraint constraintWithItem:image2 attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:image2.superview attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
    }

    // constraints for landscape orientation
    // make sure they don't conflict with and complement the existing constraints
    if (!self.layoutConstraintsLandscape) {
        UIView *image1 = self.image1;
        UIView *image2 = self.image2;
        self.layoutConstraintsLandscape = [[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[image1]-[image2]-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(image1, image2)] mutableCopy];
        [self.layoutConstraintsLandscape addObject:[NSLayoutConstraint constraintWithItem:image1 attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:image1.superview attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
        [self.layoutConstraintsLandscape addObject:[NSLayoutConstraint constraintWithItem:image2 attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem: image2.superview attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
    }

    BOOL isPortrait = UIInterfaceOrientationIsPortrait(self.interfaceOrientation);
    [self.view removeConstraints:isPortrait ? self.layoutConstraintsLandscape : self.layoutConstraintsPortrait];
    [self.view addConstraints:isPortrait ? self.layoutConstraintsPortrait : self.layoutConstraintsLandscape];        
}

现在,您需要做的就是在情况发生变化时触发约束更新。重写 willAnimateRotationToInterfaceOrientation:duration: 来在旋转屏幕时动画更新约束:
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
    [super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];

    [self.view setNeedsUpdateConstraints];

}

1
您不需要手动“触发”约束更新。'updateViewConstraints'方法已经完成了这个操作。Rokridi在下面也提到了这一点,但是这个答案并没有被编辑过。 - Ed Chin
不要忘记,在您的视图处于后台时,设备可能会旋转。当应用程序位于前台时,您可以使用setNeedsUpdateConstraints - Ben Packard
6
@knl,尺寸类不能解决iPad上的纵向和横向问题(它们都是wRegular / hRegular)。 - jcaron
@jcaron 是的,我在上面提到过这个问题。通常情况下,设计选择不应该受设备方向的影响,而应该受其大小类别的影响。如果您确实需要方向信息,仍然可以使用 UIDevice.currentDevice().orientation 和上述方法。 - knl
当我有UIView子类时,如何执行相同的操作?而不是UIViewController子类。 - Mohammad Zaid Pathan
显示剩余4条评论

17

我正在使用的方法(好的或坏的)是在故事版编辑器中定义两组约束(纵向和横向)。

为避免故事板错误提示,我将其中一组全部设置为999优先级,这样它就不会到处显示红色。

然后我将所有约束添加到出口集合中:

@property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *portraitConstraints;
@property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *landscapeConstraints;

最后,我实现了我的ViewControllersviewWillLayout方法:

- (void) viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];
    for (NSLayoutConstraint *constraint in self.portraitConstraints) {
        constraint.active = (UIApplication.sharedApplication.statusBarOrientation == UIDeviceOrientationPortrait);
    }
    for (NSLayoutConstraint *constraint in self.landscapeConstraints) {
        constraint.active = (UIApplication.sharedApplication.statusBarOrientation != UIDeviceOrientationPortrait);
    }
}

这似乎可行。我真的希望你可以在Storyboard编辑器中设置默认的活动属性。


非常好,谢谢!不幸的是,我也不得不这样做,因为我支持iOS 7,所以还不能充分利用Size classes。我有一个自定义子视图,在覆盖的“updateConstraints”中做与您在“viewWillLayoutSubviews”中相同的操作,然后我只需要从“willAnimateRotationToInterfaceOrientation”调用setNeedsUpdateConstraints即可。它确实可以正常工作。 - bandejapaisa
我也想再给你一个赞,因为你设置了优先级以避免警告。 - bandejapaisa
1
注意:active属性仅适用于iOS 8。同时,我还发现了一些方便的类方法,如NSLayoutConstraint.activateConstraints()和NSLayoutConstraint.deactivateConstraints(),你可以通过IBOutletCollection传递它们。在iOS 7中,几乎是相同的技术,但我使用self.removeConstraint和self.addConstraint,在我的情况下工作良好,但我不知道这对所有情况是否可行。 - bandejapaisa
我为所有人留下了1000的优先级,只是在界面构建器中取消了“已安装”的选项。 - Dmytro Rostopira

2

我采用与你相同的方法(没有nib文件或storyboards)。您需要在updateViewConstraints方法中更新约束(通过检查设备方向)。在updateViewConstraints中不需要调用setNeedsUpdateConstraints,因为一旦更改设备方向,最后一个方法会自动调用。


1

如果有人正在寻找当前的可能解决方案(Swift),请在此UIViewController函数中更新您的约束:

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

每次设备旋转时都会调用此函数。它允许您响应这些更改甚至对其进行动画处理。 为了给您更好的概述,这是我在上一个项目中如何通过更改两个子视图的约束来更改布局的。在纵向模式下,我的子视图相互叠放,而在横向模式下,子视图并排显示。
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    // 1. I recommend to remove existing constraints BEFORE animation, otherwise Xcode can yell at you "Unable to simultaneously satisfy constraints"
    removeConstraintsOfSubview()

    coordinator.animate(alongsideTransition: { [unowned self] context in
        // 2. By comparing the size, you can know whether you should use portrait or landscape layout and according to that remake or add specific constraints
        if size.height > size.width {
            self.setPortraitLayout()
        } else {
            self.setLandscapeLayout()
        }
        // 3. If you want the change to be smoothly animated call this block here
        UIView.animate(withDuration: context.transitionDuration) {
            self.view.layoutIfNeeded()
        }
        }, completion: { _ in
            // After the device is rotated and new constraints are added, you can perform some last touch here (probably some extra animation)
    })
}

0

你可以将约束保存为纵向和横向版本,然后在旋转时设置和移除它们。

我用xib创建了初始视图中的约束,并将其分配到视图控制器中的输出口。在旋转时,我创建替代约束,删除这些输出口但保留它们,并插入替代约束。

旋转回去时反转这个过程。


当应用程序不在屏幕上时,设备旋转会发生什么?还有其他边缘情况需要注意吗? - Ben Packard
1
当视图不可见时,您的视图将不会接收旋转通知。我非常确定这一点。事实上,在搜索相关解决方案时,我记得在某些线程中看到了这一点。我想您需要在viewWillAppear中检查设备方向。 - Dean Davids

-1

我的想法是通过改变约束优先级来处理方向。
假设优先级如下:

  • 横屏:910活动/10非活动。
  • 竖屏:920活动/20非活动。

步骤1:在Storyboard中创建横向(或纵向)设计并添加约束。
步骤2:对于仅在横屏模式下活动的约束,将其优先级设置为10。
步骤3:添加纵向模式的约束并将它们的优先级设置为920。

willAnimateRotationToInterfaceOrientation中添加代码:

for (NSLayoutConstraint *constraint in myView.constraints) {
    if (UIInterfaceOrientationIsLandscape(UIApplication.sharedApplication.statusBarOrientation)) {
        if (constraint.priority == 10)  constraint.priority = 910;
        if (constraint.priority == 920) constraint.priority = 20;
    } else {
        if (constraint.priority == 20)  constraint.priority = 920;
        if (constraint.priority == 910) constraint.priority = 10;
    }
}

这种方法的优点在于可以轻松地在界面构建器中进行调整。当我们需要切换到任何方向时,我们按优先级选择所有约束并同时更改它们(910->10,20->920):

enter image description here

界面将自动重建。


约束优先级似乎允许声明约束条件,以便应用程序自动处理它们。但是您仍然需要在每次旋转操作中手动检查它们。 - Vyachaslav Gerchicov
约束优先级不应该在声明时确定,而可以在旋转时更改。这种方法比其他任何方法都使用更少的代码。 - Igor

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