iOS 自动布局自适应 UI 问题



我想根据宽度是否大于高度来更改布局。但是我不断收到布局警告。我在自适应布局上观看了两个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)>

你是对的,你做约束工作太早了。<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().

另外,请记住在初始布局中检查方向 - 您打电话给layoutPortrait()viewDidLoad()当然认为这不是问题。

新/更改方法:

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

如果布局约束问题是由于固定(或以编程方式修改)宽度或高度约束与UIView-Encapsulated-Layout-WidthUIView-Encapsulated-Layout-Height约束冲突造成的,则问题是该牢不可破的约束导致太大的必需(优先级 1000)约束被抛出。

您经常可以通过简单地降低冲突约束的优先级(可能只是降低到 999)来解决此问题,这将适应任何临时冲突,直到旋转完成并且后续布局将以您需要的宽度或高度继续进行。

据我所知,警告实际上是因为约束移动得太晚了。旋转到横向时会出现警告 - 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)            
} 

我想出了与@Samantha大致相同的答案。但是,在打电话给super之前,我不需要打电话给deactivate。这是使用特征集合的替代方法,但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)
}

这是有问题的部分:

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)

最新更新