具有自定义约束的 UIStackView - "Unable to simultaneously satisfy constraints" 更改轴时
UIStackView with custom constraints - "Unable to simultaneously satisfy constraints" when changing axis
我偶然发现了 UIStackView
的问题。
它与 UIStackView 及其排列的子视图之间存在一些自定义约束的情况有关。更改堆栈视图的轴和自定义约束时,错误消息 [LayoutConstraints] Unable to simultaneously satisfy constraints.
出现在控制台中。
这是一个代码示例,您可以将其粘贴到新项目中以观察问题:
import UIKit
public class ViewController: UIViewController {
private var stateToggle = false
private let viewA: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.accessibilityIdentifier = "viewA"
view.backgroundColor = .red
return view
}()
private let viewB: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.accessibilityIdentifier = "viewB"
view.backgroundColor = .green
return view
}()
private lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [viewA, viewB])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.distribution = .fill
stackView.alignment = .fill
stackView.accessibilityIdentifier = "stackView"
return stackView
}()
private lazy var viewAOneFourthOfStackViewHightConstraint = viewA.heightAnchor.constraint(equalTo: stackView.heightAnchor, multiplier: 0.25)
private lazy var viewAOneFourthOfStackViewWidthConstraint = viewA.widthAnchor.constraint(equalTo: stackView.widthAnchor, multiplier: 0.25)
public override func viewDidLoad() {
super.viewDidLoad()
view.accessibilityIdentifier = "rootView"
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
let button = UIButton(type: .system)
button.frame = .init(x: 50, y: 50, width: 100, height: 44)
button.setTitle("toggle layout", for: .normal)
button.tintColor = .white
button.backgroundColor = .black
button.addTarget(self, action: #selector(buttonTap), for: .touchUpInside)
view.addSubview(button)
updateConstraintsBasedOnState()
}
@objc func buttonTap() {
stateToggle.toggle()
updateConstraintsBasedOnState()
}
private func updateConstraintsBasedOnState() {
switch (stateToggle) {
case true:
stackView.axis = .horizontal
viewAOneFourthOfStackViewHightConstraint.isActive = false
viewAOneFourthOfStackViewWidthConstraint.isActive = true
case false:
stackView.axis = .vertical
viewAOneFourthOfStackViewWidthConstraint.isActive = false
viewAOneFourthOfStackViewHightConstraint.isActive = true
}
}
}
在这个例子中,我们制作了一个包含两个子视图的堆栈视图。我们希望其中一个占据 25% 的区域 - 所以我们有实现这一目标的约束(每个方向一个),但我们需要能够相应地切换它们。当切换发生时(在这种情况下从垂直堆叠到水平堆叠),出现错误信息:
[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:0x60000089ca00 stackView.leading == UILayoutGuide:0x6000012b48c0'UIViewSafeAreaLayoutGuide'.leading (active, names: stackView:0x7fe269d07b50 )>",
"<NSLayoutConstraint:0x60000089c9b0 stackView.trailing == UILayoutGuide:0x6000012b48c0'UIViewSafeAreaLayoutGuide'.trailing (active, names: stackView:0x7fe269d07b50 )>",
"<NSLayoutConstraint:0x60000089c730 viewA.width == 0.25*stackView.width (active, names: viewA:0x7fe269e14fd0, stackView:0x7fe269d07b50 )>",
"<NSLayoutConstraint:0x6000008ba990 'UISV-canvas-connection' stackView.leading == viewA.leading (active, names: stackView:0x7fe269d07b50, viewA:0x7fe269e14fd0 )>",
"<NSLayoutConstraint:0x6000008ba9e0 'UISV-canvas-connection' H:[viewA]-(0)-| (active, names: stackView:0x7fe269d07b50, viewA:0x7fe269e14fd0, '|':stackView:0x7fe269d07b50 )>",
"<NSLayoutConstraint:0x6000008bab20 'UIView-Encapsulated-Layout-Width' rootView.width == 375 (active, names: rootView:0x7fe269c111a0 )>",
"<NSLayoutConstraint:0x60000089ce60 'UIViewSafeAreaLayoutGuide-left' H:|-(0)-[UILayoutGuide:0x6000012b48c0'UIViewSafeAreaLayoutGuide'](LTR) (active, names: rootView:0x7fe269c111a0, '|':rootView:0x7fe269c111a0 )>",
"<NSLayoutConstraint:0x60000089cdc0 'UIViewSafeAreaLayoutGuide-right' H:[UILayoutGuide:0x6000012b48c0'UIViewSafeAreaLayoutGuide']-(0)-|(LTR) (active, names: rootView:0x7fe269c111a0, '|':rootView:0x7fe269c111a0 )>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x6000008ba990 'UISV-canvas-connection' stackView.leading == viewA.leading (active, names: stackView:0x7fe269d07b50, viewA:0x7fe269e14fd0 )>
注意约束:"<NSLayoutConstraint:0x6000008ba9e0 'UISV-canvas-connection' H:[viewA]-(0)-| (active, names: stackView:0x7fe269d07b50, viewA:0x7fe269e14fd0, '|':stackView:0x7fe269d07b50 )>"
,它表示 “viewA 的尾部应与堆栈视图的尾部相同”。这不是水平轴布局的有效约束。但这不是我添加的约束,它是堆栈视图内部的东西 (UISV-canvas-connection
)。
在我看来,因为我们是 activating/deactivating 自定义约束,而且堆栈视图在切换轴时在内部执行相同的操作 - 暂时存在冲突和错误。
可能的解决方案/解决方法:
- 解决方法 1:使自定义约束具有
priority = 999
。这不是一个好的解决方法 - 不仅因为它是 hacky,而且因为在某些情况下它会导致其他布局问题(例如,当 viewA 有一些内部布局要求冲突时,例如具有所需拥抱优先级的子视图)。
- 解决方法2:删除轴变化前的排列视图,并在轴变化后重新添加它们。这可行,但它也很 hacky,对于某些复杂的情况在实践中可能很困难。
- 解决方法 3:根本不使用
UIStackView
- 使用常规 UIView
作为容器实施并根据需要创建所需的约束。
如果这应该被视为错误(并报告给 Apple)以及是否有其他(更好的?)解决方案,您有什么想法吗?
例如,有没有办法告诉 AutoLayout -“我要更改一些约束以及堆栈视图的轴 - 等我完成后再继续评估布局”。
据我所知,您和 iOS 都有需要停用和激活的限制。为了正确执行并避免冲突,您和 iOS 都需要先停用旧约束,然后才能开始激活新约束。
这是一个有效的解决方案。停用旧约束后,告诉 stackView
到 layoutIfNeeded()
。然后它将按顺序获得其约束,您可以激活新约束。
private func updateConstraintsBasedOnState() {
switch (stateToggle) {
case true:
stackView.axis = .horizontal
viewAOneFourthOfStackViewHightConstraint.isActive = false
stackView.layoutIfNeeded()
viewAOneFourthOfStackViewWidthConstraint.isActive = true
case false:
stackView.axis = .vertical
viewAOneFourthOfStackViewWidthConstraint.isActive = false
stackView.layoutIfNeeded()
viewAOneFourthOfStackViewHightConstraint.isActive = true
}
}
虽然 vacawama 的回答可以解决问题,但还有两个其他选项...
1 - 为约束赋予小于 .required
的优先级,以允许 auto-layout 暂时打破约束而无需抱怨。
在viewDidLoad()
中添加:
viewAOneFourthOfStackViewHightConstraint.priority = .defaultHigh
viewAOneFourthOfStackViewWidthConstraint.priority = .defaultHigh
并保持原样 updateConstraintsBasedOnState()
,或者
2 - 使用不同的优先级,并交换它们而不是停用/激活约束。
在viewDidLoad()
中添加:
viewAOneFourthOfStackViewHightConstraint.priority = .defaultHigh
viewAOneFourthOfStackViewWidthConstraint.priority = .defaultLow
viewAOneFourthOfStackViewHightConstraint.isActive = true
viewAOneFourthOfStackViewWidthConstraint.isActive = true
并将 updateConstraintsBasedOnState()
更改为:
private func updateConstraintsBasedOnState() {
switch (stateToggle) {
case true:
stackView.axis = .horizontal
viewAOneFourthOfStackViewHightConstraint.priority = .defaultLow
viewAOneFourthOfStackViewWidthConstraint.priority = .defaultHigh
case false:
stackView.axis = .vertical
viewAOneFourthOfStackViewWidthConstraint.priority = .defaultLow
viewAOneFourthOfStackViewHightConstraint.priority = .defaultHigh
}
}
我不知道改变优先级是否比改变激活更有效率?但可能需要考虑。
我偶然发现了 UIStackView
的问题。
它与 UIStackView 及其排列的子视图之间存在一些自定义约束的情况有关。更改堆栈视图的轴和自定义约束时,错误消息 [LayoutConstraints] Unable to simultaneously satisfy constraints.
出现在控制台中。
这是一个代码示例,您可以将其粘贴到新项目中以观察问题:
import UIKit
public class ViewController: UIViewController {
private var stateToggle = false
private let viewA: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.accessibilityIdentifier = "viewA"
view.backgroundColor = .red
return view
}()
private let viewB: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.accessibilityIdentifier = "viewB"
view.backgroundColor = .green
return view
}()
private lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [viewA, viewB])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.distribution = .fill
stackView.alignment = .fill
stackView.accessibilityIdentifier = "stackView"
return stackView
}()
private lazy var viewAOneFourthOfStackViewHightConstraint = viewA.heightAnchor.constraint(equalTo: stackView.heightAnchor, multiplier: 0.25)
private lazy var viewAOneFourthOfStackViewWidthConstraint = viewA.widthAnchor.constraint(equalTo: stackView.widthAnchor, multiplier: 0.25)
public override func viewDidLoad() {
super.viewDidLoad()
view.accessibilityIdentifier = "rootView"
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
stackView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
let button = UIButton(type: .system)
button.frame = .init(x: 50, y: 50, width: 100, height: 44)
button.setTitle("toggle layout", for: .normal)
button.tintColor = .white
button.backgroundColor = .black
button.addTarget(self, action: #selector(buttonTap), for: .touchUpInside)
view.addSubview(button)
updateConstraintsBasedOnState()
}
@objc func buttonTap() {
stateToggle.toggle()
updateConstraintsBasedOnState()
}
private func updateConstraintsBasedOnState() {
switch (stateToggle) {
case true:
stackView.axis = .horizontal
viewAOneFourthOfStackViewHightConstraint.isActive = false
viewAOneFourthOfStackViewWidthConstraint.isActive = true
case false:
stackView.axis = .vertical
viewAOneFourthOfStackViewWidthConstraint.isActive = false
viewAOneFourthOfStackViewHightConstraint.isActive = true
}
}
}
在这个例子中,我们制作了一个包含两个子视图的堆栈视图。我们希望其中一个占据 25% 的区域 - 所以我们有实现这一目标的约束(每个方向一个),但我们需要能够相应地切换它们。当切换发生时(在这种情况下从垂直堆叠到水平堆叠),出现错误信息:
[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:0x60000089ca00 stackView.leading == UILayoutGuide:0x6000012b48c0'UIViewSafeAreaLayoutGuide'.leading (active, names: stackView:0x7fe269d07b50 )>",
"<NSLayoutConstraint:0x60000089c9b0 stackView.trailing == UILayoutGuide:0x6000012b48c0'UIViewSafeAreaLayoutGuide'.trailing (active, names: stackView:0x7fe269d07b50 )>",
"<NSLayoutConstraint:0x60000089c730 viewA.width == 0.25*stackView.width (active, names: viewA:0x7fe269e14fd0, stackView:0x7fe269d07b50 )>",
"<NSLayoutConstraint:0x6000008ba990 'UISV-canvas-connection' stackView.leading == viewA.leading (active, names: stackView:0x7fe269d07b50, viewA:0x7fe269e14fd0 )>",
"<NSLayoutConstraint:0x6000008ba9e0 'UISV-canvas-connection' H:[viewA]-(0)-| (active, names: stackView:0x7fe269d07b50, viewA:0x7fe269e14fd0, '|':stackView:0x7fe269d07b50 )>",
"<NSLayoutConstraint:0x6000008bab20 'UIView-Encapsulated-Layout-Width' rootView.width == 375 (active, names: rootView:0x7fe269c111a0 )>",
"<NSLayoutConstraint:0x60000089ce60 'UIViewSafeAreaLayoutGuide-left' H:|-(0)-[UILayoutGuide:0x6000012b48c0'UIViewSafeAreaLayoutGuide'](LTR) (active, names: rootView:0x7fe269c111a0, '|':rootView:0x7fe269c111a0 )>",
"<NSLayoutConstraint:0x60000089cdc0 'UIViewSafeAreaLayoutGuide-right' H:[UILayoutGuide:0x6000012b48c0'UIViewSafeAreaLayoutGuide']-(0)-|(LTR) (active, names: rootView:0x7fe269c111a0, '|':rootView:0x7fe269c111a0 )>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x6000008ba990 'UISV-canvas-connection' stackView.leading == viewA.leading (active, names: stackView:0x7fe269d07b50, viewA:0x7fe269e14fd0 )>
注意约束:"<NSLayoutConstraint:0x6000008ba9e0 'UISV-canvas-connection' H:[viewA]-(0)-| (active, names: stackView:0x7fe269d07b50, viewA:0x7fe269e14fd0, '|':stackView:0x7fe269d07b50 )>"
,它表示 “viewA 的尾部应与堆栈视图的尾部相同”。这不是水平轴布局的有效约束。但这不是我添加的约束,它是堆栈视图内部的东西 (UISV-canvas-connection
)。
在我看来,因为我们是 activating/deactivating 自定义约束,而且堆栈视图在切换轴时在内部执行相同的操作 - 暂时存在冲突和错误。
可能的解决方案/解决方法:
- 解决方法 1:使自定义约束具有
priority = 999
。这不是一个好的解决方法 - 不仅因为它是 hacky,而且因为在某些情况下它会导致其他布局问题(例如,当 viewA 有一些内部布局要求冲突时,例如具有所需拥抱优先级的子视图)。 - 解决方法2:删除轴变化前的排列视图,并在轴变化后重新添加它们。这可行,但它也很 hacky,对于某些复杂的情况在实践中可能很困难。
- 解决方法 3:根本不使用
UIStackView
- 使用常规UIView
作为容器实施并根据需要创建所需的约束。
如果这应该被视为错误(并报告给 Apple)以及是否有其他(更好的?)解决方案,您有什么想法吗?
例如,有没有办法告诉 AutoLayout -“我要更改一些约束以及堆栈视图的轴 - 等我完成后再继续评估布局”。
据我所知,您和 iOS 都有需要停用和激活的限制。为了正确执行并避免冲突,您和 iOS 都需要先停用旧约束,然后才能开始激活新约束。
这是一个有效的解决方案。停用旧约束后,告诉 stackView
到 layoutIfNeeded()
。然后它将按顺序获得其约束,您可以激活新约束。
private func updateConstraintsBasedOnState() {
switch (stateToggle) {
case true:
stackView.axis = .horizontal
viewAOneFourthOfStackViewHightConstraint.isActive = false
stackView.layoutIfNeeded()
viewAOneFourthOfStackViewWidthConstraint.isActive = true
case false:
stackView.axis = .vertical
viewAOneFourthOfStackViewWidthConstraint.isActive = false
stackView.layoutIfNeeded()
viewAOneFourthOfStackViewHightConstraint.isActive = true
}
}
虽然 vacawama 的回答可以解决问题,但还有两个其他选项...
1 - 为约束赋予小于 .required
的优先级,以允许 auto-layout 暂时打破约束而无需抱怨。
在viewDidLoad()
中添加:
viewAOneFourthOfStackViewHightConstraint.priority = .defaultHigh
viewAOneFourthOfStackViewWidthConstraint.priority = .defaultHigh
并保持原样 updateConstraintsBasedOnState()
,或者
2 - 使用不同的优先级,并交换它们而不是停用/激活约束。
在viewDidLoad()
中添加:
viewAOneFourthOfStackViewHightConstraint.priority = .defaultHigh
viewAOneFourthOfStackViewWidthConstraint.priority = .defaultLow
viewAOneFourthOfStackViewHightConstraint.isActive = true
viewAOneFourthOfStackViewWidthConstraint.isActive = true
并将 updateConstraintsBasedOnState()
更改为:
private func updateConstraintsBasedOnState() {
switch (stateToggle) {
case true:
stackView.axis = .horizontal
viewAOneFourthOfStackViewHightConstraint.priority = .defaultLow
viewAOneFourthOfStackViewWidthConstraint.priority = .defaultHigh
case false:
stackView.axis = .vertical
viewAOneFourthOfStackViewWidthConstraint.priority = .defaultLow
viewAOneFourthOfStackViewHightConstraint.priority = .defaultHigh
}
}
我不知道改变优先级是否比改变激活更有效率?但可能需要考虑。