iOS自动布局自适应UI问题

9

如果宽度大于高度,我想要更改我的布局。但是我一直收到布局警告。我观看了WWDC有关自适应布局的两个视频,这很有帮助,但并没有解决问题。我使用以下代码创建了我的布局的简单版本。这只是为了让人们能够复制我的问题。代码在iPhone 7 Plus上运行。

import UIKit

class ViewController: UIViewController {

    var view1: UIView!
    var view2: UIView!

    var constraints = [NSLayoutConstraint]()

    override func viewDidLoad() {
        super.viewDidLoad()

        view1 = UIView()
        view1.translatesAutoresizingMaskIntoConstraints = false
        view1.backgroundColor = UIColor.red

        view2 = UIView()
        view2.translatesAutoresizingMaskIntoConstraints = false
        view2.backgroundColor = UIColor.green

        view.addSubview(view1)
        view.addSubview(view2)

        layoutPortrait()
    }

    func layoutPortrait() {
        let views = [
            "a1": view1,
            "a2": view2
        ]

        constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[a1]|", options: [], metrics: nil, views: views)
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[a2]|", options: [], metrics: nil, views: views)
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1(450)][a2]|", options: [], metrics: nil, views: views)
        NSLayoutConstraint.activate(constraints)
    }

    func layoutLandscape() {
        let views = [
            "a1": view1,
            "a2": view2
        ]

        constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[a1(a2)][a2(a1)]|", options: [], metrics: nil, views: views)
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1]|", options: [], metrics: nil, views: views)
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a2]|", options: [], metrics: nil, views: views)
    NSLayoutConstraint.activate(constraints)
    }

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

         NSLayoutConstraint.deactivate(constraints)

         constraints.removeAll()

         if (size.width > size.height) {
             layoutLandscape()
         } else {
            layoutPortrait()
         }

     } 

}

当我旋转几次时,xcode会记录警告。我认为我把布局切换得太早了,因为它仍在改变,但我已经将纵向高度设置得太大或其他什么的。有人知道我做错了什么吗?

约束警告:(从横向返回纵向时发生)

[LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<NSLayoutConstraint:0x618000081900 V:|-(0)-[UIView:0x7ff68ee0ac40]   (active, names: '|':UIView:0x7ff68ec03f50 )>",
    "<NSLayoutConstraint:0x618000081d60 UIView:0x7ff68ee0ac40.height == 450   (active)>",
    "<NSLayoutConstraint:0x618000083cf0 V:[UIView:0x7ff68ee0ac40]-(0)-[UIView:0x7ff68ee0ade0]   (active)>",
    "<NSLayoutConstraint:0x618000083d40 V:[UIView:0x7ff68ee0ade0]-(0)-|   (active, names: '|':UIView:0x7ff68ec03f50 )>",
    "<NSLayoutConstraint:0x600000085d70 'UIView-Encapsulated-Layout-Height' UIView:0x7ff68ec03f50.height == 414   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x618000081d60 UIView:0x7ff68ee0ac40.height == 450   (active)>

为什么不尝试使用锚点布局?--> 苹果文档 - dahiya_boy
iOS 8需要被支持。 - Haagenti
抱歉,我犯了错误,它支持9.x版本。你是正确的。对不起 :( - dahiya_boy
你已经完成了使用 VFL ,你甚至可以尝试使用NSLayoutConstraints,就像这个 ios_auto_layouts 链接中所示的那样,但代码是用 obj-c 写的。我从不建议使用 VFL ,因为设备会根据自身设置约束,并且通常会显示错误,就像你正在遇到的问题一样。这也发生在我身上,这就是为什么我说,对于其他人我不能说。 - dahiya_boy
为什么不直接调用 setNeedsUpdateConstraints 并从 updateConstraints 更新您的约束呢?这应该可以处理链式旋转的冲突。 - Sulthan
显示剩余4条评论
5个回答

4
您是正确的,您将约束工作做得太早了。 <NSLayoutConstraint:0x600000085d70 'UIView-Encapsulated-Layout-Height' UIView:0x7ff68ec03f50.height == 414... 是系统创建的用于视图控制器视图在横向模式下的高度约束 - 为什么是414,正好是iPhone 5.5(6/7 Plus)横向模式下的高度。旋转尚未开始,与450高度约束冲突的垂直约束将导致警告。
因此,在旋转完成后添加约束。 viewWillLayoutSubviews 是逻辑上的位置,因为在屏幕上出现任何内容之前,此时调用视图大小已准备就绪,并且可以节省系统所需进行的布局传递。
但是,为了消除警告,您仍然需要在 viewWillTransition(to size: with coordinator:) 中删除约束。在调用viewWillLayoutSubviews()之前,系统会进行新的自动布局计算,并且当从纵向切换到横向时,会以相反的方式发出警告。
编辑:如评论中@sulthan所指出的那样,由于其他事物可能会触发布局,因此您还应始终在添加约束之前删除它们。如果[constraints]数组已清空,则没有效果。由于在停用后清空数组是一个关键步骤,因此应该有一个自己的方法clearConstraints()
另外,请记住在初始布局中检查方向 - 您在viewDidLoad()中调用了layoutPortrait(),但这当然不是问题所在。
新/更改的方法:
override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()

    // Should be inside layoutPortrait/Landscape methods, here to keep code sample short.
    clearConstraints() 

    if (self.view.frame.size.width > self.view.frame.size.height) {
        layoutLandscape()
    } else {
        layoutPortrait()
    }
}

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

    clearConstraints()
}

/// Ensure array is emptied when constraints are deactivated. Can be called repeatedly. 
func clearConstraints() {
    NSLayoutConstraint.deactivate(constraints) 
    constraints.removeAll()
}

2
我认为你应该在viewWillLayoutSubviews中始终删除约束,因为有时视图会在不进行转换的情况下布局。 - Sulthan
@Sulthan 这是一个很好的观点。viewWillTransition(to size:with coordinator:)将会获取到许多信息,但其他事情也可能会触发布局传递,因此在添加新约束之前实际上应该始终调用删除约束/清空数组。我已经更新了我的答案。 - Mike Sand

1
如果布局约束问题是由于固定(或以编程方式修改的)宽度或高度约束与UIView-Encapsulated-Layout-WidthUIView-Encapsulated-Layout-Height约束冲突造成的,则是该不可打破约束导致您的太大、必需(优先级为1000)约束被抛弃的问题。
您可以通过简单降低冲突约束的优先级(可能只降到999)来经常解决问题,这将适应任何临时冲突,直到旋转完成并且随后的布局将按照您所需的宽度或高度进行。

0
据我所知,警告实际上是因为约束被移动得太晚了。当旋转到横向时,警告出现了——450的约束仍然存在,但无法满足,因为视图的高度太小了。
我尝试了几种方法,似乎最好的方法是在较小的视图上设置约束,这样较大视图的高度仍为450。
与其...
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1(450)][a2]|", options: [], metrics: nil, views: views)

我使用了

let height = view.frame.height - 450
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1][a2(\(height))]|", options: [], metrics: nil, views: views)

然后在viewWillTransitionToSize方法中:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    NSLayoutConstraint.deactivate(self.constraints)
    self.constraints.removeAll()

    coordinator.animate(alongsideTransition: { (context) in

        if (size.width > size.height) {
            self.layoutLandscape()
        } else {
            self.layoutPortrait()
        }
    }, completion: nil)

} 

编辑:

对我有效的另一种方法(也许是更一般的答案,尽管我不确定是否建议这样做):在调用super.viewWillTransition之前停用约束,然后沿着转换一起动画化其余部分。我的理由是,在旋转开始之前,约束立即被移除,因此一旦应用程序开始旋转到横向模式(此时旧的高度约束将无法满足),约束已经被停用,新的约束可以生效。

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        NSLayoutConstraint.deactivate(constraints)
        super.viewWillTransition(to: size, with: coordinator)
        constraints.removeAll()

        coordinator.animate(alongsideTransition: { (context) in
            if (size.width > size.height) {
                self.layoutLandscape()
            } else {
                self.layoutPortrait()
            }

        }, completion: nil)            
    } 

我明白你的意思。但这并不能完全解决问题。我的例子是简化的,你的答案没有解决如何在纵向和横向旋转并使用另一种布局的问题。 - Haagenti
我编辑了我的答案,添加了另一种(可能更通用)适用于我的方法。这是你想要的吗? - Samantha

0

我得出的答案与 @Samantha 很相似。在调用 super 之前,我不需要调用 deactivate。这是使用 trait 集合的另一种方法,但是 transitionToSize 方法的效果类似。

override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
    super.willTransition(to: newCollection, with: coordinator)
    NSLayoutConstraint.deactivate(constraints)

    constraints.removeAll()

    coordinator.animate(alongsideTransition: { context in
        if newCollection.verticalSizeClass == .compact {
            self.layoutLandscape()
        } else {
            self.layoutPortrait()
        }
    }, completion: nil)
}

0

这是有问题的部分:

constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1(450)][a2]|", options: [], metrics: nil, views: views)

这行代码生成了4个约束条件:

  1. 距离顶部的 a1 距离为 0
  2. a1 的高度为 450
  3. a1a2 的距离为 0
  4. a2 到底部的距离为 0

这四个约束条件在横屏模式下会一起失效,因为屏幕的总高度将小于 450

现在你正在更新约束条件,然后才切换到横屏模式,这在从纵向切换到横向时有效。但是当你从横向切换到纵向时,你的约束条件太早了,短暂的时间内,你仍处于横向状态,触发了警告。

请注意,你的约束条件本质上没有任何问题。如果你想解决警告问题,我认为最好的解决方案是降低其中一个约束条件的优先级,例如高度约束条件:

constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1(450@999)][a2]|", options: [], metrics: nil, views: views)

尽管这解决了问题,但我觉得Mike Sand的答案更加简洁明了。因为如果按照我的方法,需要在约束条件中添加很多@ 999。不过还是谢谢,我学到了很多。 - Haagenti

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