UIStackView con restricciones personalizadas: "No se pueden satisfacer simultáneamente las restricciones" al cambiar el eje
Me he topado con un problema con UIStackView
. Tiene que ver con el caso en que existen algunas restricciones personalizadas entre UIStackView y sus subvistas organizadas. Al cambiar el eje de la vista de pila y las restricciones personalizadas, [LayoutConstraints] Unable to simultaneously satisfy constraints.
aparece el mensaje de error en la consola.
Aquí hay un ejemplo de código que puede pegar en un nuevo proyecto para observar el problema:
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
}
}
}
En este ejemplo, estamos creando una vista de pila con dos subvistas. Queremos que uno de ellos ocupe el 25 % del área, por lo que tenemos las restricciones para lograrlo (uno para cada orientación), pero debemos poder cambiarlos en consecuencia. Cuando ocurre el cambio (de pila vertical a horizontal en este caso), aparece el mensaje de error:
[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 )>
Observe la restricción: "<NSLayoutConstraint:0x6000008ba9e0 'UISV-canvas-connection' H:[viewA]-(0)-| (active, names: stackView:0x7fe269d07b50, viewA:0x7fe269e14fd0, '|':stackView:0x7fe269d07b50 )>"
que dice "el seguimiento de viewA debe ser el mismo que el seguimiento de la vista de pila" . Esta no es una restricción válida para el diseño del eje horizontal. Pero no es una restricción que haya agregado, es algo interno a la vista de pila ( UISV-canvas-connection
).
Me parece que debido a que estamos activando/desactivando la restricción personalizada y también la vista de pila hace lo mismo internamente al cambiar de eje, hay un conflicto y un error temporalmente.
Posibles soluciones / soluciones alternativas:
- Solución alternativa 1: haga que las restricciones personalizadas tengan
priority = 999
. Esta no es una buena solución, no solo porque es un truco, sino también porque en algunos casos daría lugar a otros problemas de diseño (por ejemplo, cuando viewA tiene algunos requisitos de diseño internos que entran en conflicto, como subvistas con prioridad de abrazo requerida). - Solución alternativa 2: elimine las vistas organizadas antes del cambio de eje y vuelva a agregarlas después del cambio de eje. Esto funciona, pero también es complicado y puede ser difícil en la práctica para algunos casos complicados.
- Solución alternativa 3: no lo use
UIStackView
en absoluto: implemente el uso de regularUIView
como contenedor y cree las restricciones requeridas según sea necesario.
¿Alguna idea si esto debería considerarse un error (e informar a Apple) y si hay otras soluciones (¿mejores?)?
Por ejemplo, ¿hay alguna manera de decirle a AutoLayout: "Estoy a punto de cambiar algunas restricciones y también el eje de una vista de pila; solo espere hasta que termine y luego continúe evaluando el diseño".
Respuestas
Tal como lo veo, tanto usted como iOS tienen restricciones que debe desactivar y activar. Para hacerlo correctamente y evitar conflictos, tanto usted como iOS deben desactivar sus restricciones anteriores antes de poder comenzar a activar sus nuevas restricciones.
Aquí hay una solución que funciona. Dígale stackView
a layoutIfNeeded()
después de haber desactivado su antigua restricción. Luego obtendrá sus restricciones en orden y podrá activar su nueva restricción.
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
}
}
Si bien la respuesta de vacawama hará el trabajo, otras dos opciones ...
1 - Dé a las restricciones una Prioridad menor que .required
para permitir que el diseño automático rompa temporalmente la restricción sin quejarse.
Agrega esto en viewDidLoad()
:
viewAOneFourthOfStackViewHightConstraint.priority = .defaultHigh
viewAOneFourthOfStackViewWidthConstraint.priority = .defaultHigh
y déjelo updateConstraintsBasedOnState()
como lo tenía originalmente, o
2 - Use diferentes niveles de prioridad e intercámbielos en lugar de desactivar/activar restricciones.
Agrega esto en viewDidLoad()
:
viewAOneFourthOfStackViewHightConstraint.priority = .defaultHigh
viewAOneFourthOfStackViewWidthConstraint.priority = .defaultLow
viewAOneFourthOfStackViewHightConstraint.isActive = true
viewAOneFourthOfStackViewWidthConstraint.isActive = true
y cambiar updateConstraintsBasedOnState()
a esto:
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
}
}
No sé si cambiar prioridades es más o menos eficiente que cambiar activaciones. Pero podría ser algo a considerar.