UIStackView с настраиваемыми ограничениями - «Невозможно одновременно удовлетворить ограничения» при изменении оси

Aug 20 2020

Я наткнулся на проблему с 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).

Мне кажется, что из-за того, что мы активируем / деактивируем пользовательское ограничение, а также представление стека внутренне делает то же самое при переключении оси - временно возникает конфликт и ошибка.

Возможные решения / обходные пути:

  • Обходной путь 1. Сделайте настраиваемые ограничения priority = 999. Это не лучший обходной путь - не только потому, что он хакерский, но и потому, что в некоторых случаях это может привести к другим проблемам с макетом (например, когда у viewA есть некоторые внутренние требования к макету, которые конфликтуют, такие как подпредставления с требуемым приоритетом обнимания).
  • Обходной путь 2. Удалите упорядоченные виды до изменения оси и повторно добавьте их после изменения оси. Это работает, но также может оказаться трудным на практике в некоторых сложных случаях.
  • Обходной путь 3: не использовать UIStackViewвообще - реализуйте, используя обычный UIViewв качестве контейнера, и при необходимости создайте необходимые ограничения.

Есть идеи, следует ли считать это ошибкой (и сообщить об этом в Apple) и есть ли другие (лучшие?) Решения?

Например, есть способ сообщить AutoLayout: «Я собираюсь изменить некоторые ограничения, а также ось представления стека - просто подождите, пока я закончу, а затем продолжите оценку макета».

Ответы

1 vacawama Aug 20 2020 at 19:11

Насколько я понимаю, и у вас, и у 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
    }
}
DonMag Aug 20 2020 at 20:10

В то время как vacawama ответ «s будет делать эту работу, два других варианта ...

1 - Присвойте ограничениям приоритет ниже, чем .requiredразрешить автоматической компоновке временно нарушить ограничение без жалоб.

Добавьте это в 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
    }
}

Не знаю, более или менее эффективно смена приоритетов, чем смена активаций? Но, возможно, стоит подумать.