iOS 自动布局自适应 UI 问题
iOS Autolayout adaptive UI problems
我想根据宽度是否大于高度来更改布局。但我不断收到布局警告。我看了两个关于自适应布局的 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)>
据我所知,这个警告实际上是因为约束被移动太迟了。旋转到横向时出现警告——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 个约束:
- 顶部到
a1
的距离是 0
。
a1
的身高是 450
。
a1
到 a2
的距离是 0
。
a2
到底部的距离是 0
.
四个约束在横向模式下一起打破,因为屏幕的整体高度将小于 450
。
您现在正在做的是在更改为横向模式之前更新约束,这在您从纵向切换到横向时有效。但是,当您从横向切换到纵向时,您更改约束的时间过早,并且在短时间内,您仍然在横向时获得纵向约束,从而触发该警告。
请注意,您的约束没有本质上的错误。如果您想修复警告,我认为最好的解决方案是降低其中一个约束的优先级,例如高度限制:
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1(450@999)][a2]|", options: [], metrics: nil, views: views)
你是对的,你做约束工作太早了。 <NSLayoutConstraint:0x600000085d70 'UIView-Encapsulated-Layout-Height' UIView:0x7ff68ec03f50.height == 414
...
是一个 system-created 约束视图控制器视图的横向高度 - 为什么它是 414,正好 iPhone 5.5(6/7 Plus)横向高度。旋转尚未开始,垂直约束与 450 高度约束冲突,您收到警告。
因此您必须在旋转完成后添加约束。 viewWillLayoutSubviews
是合乎逻辑的位置,因为它在视图大小准备就绪但在屏幕上出现任何内容之前被调用,并且可以保存系统它本应进行的布局传递。
但是,要消除警告,您仍然需要删除 viewWillTransition(to size: with coordinator:)
中的约束。系统会在调用 viewWillLayoutSubviews()
之前进行新的自动布局计算,当从纵向切换到横向时,您会以另一种方式收到警告。
编辑:正如@sulthan 在评论中指出的那样,由于其他事情可以触发布局,因此您还应该始终在添加约束之前删除它们。如果 [constraints]
数组被清空则没有效果。由于停用后清空数组是一个关键步骤,它应该在自己的方法中,所以 clearConstraints()
。
此外,请记住检查初始布局中的方向 - 您在 viewDidLoad()
中调用 layoutPortrait()
当然这不是问题所在。
New/change 方法:
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-Width
或 UIView-Encapsulated-Layout-Height
约束冲突造成的,则问题出在 unbreakable 约束导致你的太大,必需的(优先级 1000)约束被抛出。
您通常可以通过简单地降低冲突约束的优先级(可能只是到 999)来解决问题,这将适应任何临时冲突,直到旋转完成并且后续布局将按照您需要的宽度或高度进行。
我想根据宽度是否大于高度来更改布局。但我不断收到布局警告。我看了两个关于自适应布局的 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)>
据我所知,这个警告实际上是因为约束被移动太迟了。旋转到横向时出现警告——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 个约束:
- 顶部到
a1
的距离是0
。 a1
的身高是450
。a1
到a2
的距离是0
。a2
到底部的距离是0
.
四个约束在横向模式下一起打破,因为屏幕的整体高度将小于 450
。
您现在正在做的是在更改为横向模式之前更新约束,这在您从纵向切换到横向时有效。但是,当您从横向切换到纵向时,您更改约束的时间过早,并且在短时间内,您仍然在横向时获得纵向约束,从而触发该警告。
请注意,您的约束没有本质上的错误。如果您想修复警告,我认为最好的解决方案是降低其中一个约束的优先级,例如高度限制:
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1(450@999)][a2]|", options: [], metrics: nil, views: views)
你是对的,你做约束工作太早了。 <NSLayoutConstraint:0x600000085d70 'UIView-Encapsulated-Layout-Height' UIView:0x7ff68ec03f50.height == 414
...
是一个 system-created 约束视图控制器视图的横向高度 - 为什么它是 414,正好 iPhone 5.5(6/7 Plus)横向高度。旋转尚未开始,垂直约束与 450 高度约束冲突,您收到警告。
因此您必须在旋转完成后添加约束。 viewWillLayoutSubviews
是合乎逻辑的位置,因为它在视图大小准备就绪但在屏幕上出现任何内容之前被调用,并且可以保存系统它本应进行的布局传递。
但是,要消除警告,您仍然需要删除 viewWillTransition(to size: with coordinator:)
中的约束。系统会在调用 viewWillLayoutSubviews()
之前进行新的自动布局计算,当从纵向切换到横向时,您会以另一种方式收到警告。
编辑:正如@sulthan 在评论中指出的那样,由于其他事情可以触发布局,因此您还应该始终在添加约束之前删除它们。如果 [constraints]
数组被清空则没有效果。由于停用后清空数组是一个关键步骤,它应该在自己的方法中,所以 clearConstraints()
。
此外,请记住检查初始布局中的方向 - 您在 viewDidLoad()
中调用 layoutPortrait()
当然这不是问题所在。
New/change 方法:
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-Width
或 UIView-Encapsulated-Layout-Height
约束冲突造成的,则问题出在 unbreakable 约束导致你的太大,必需的(优先级 1000)约束被抛出。
您通常可以通过简单地降低冲突约束的优先级(可能只是到 999)来解决问题,这将适应任何临时冲突,直到旋转完成并且后续布局将按照您需要的宽度或高度进行。