簡體   English   中英

具有自定義約束的 UIStackView - 更改軸時“無法同時滿足約束”

[英]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 ) 的內部內容。

在我看來,因為我們正在激活/停用自定義約束,並且堆棧視圖在切換軸時在內部執行相同的操作 - 暫時存在沖突和錯誤。

潛在的解決方案/解決方法:

  • 解決方法 1:使自定義約束的priority = 999 這不是一個好的解決方法 - 不僅因為它是 hacky,而且因為在某些情況下它會導致其他布局問題(例如,當 viewA 有一些內部布局要求沖突時,例如子視圖需要擁抱優先級)。
  • 解決方法2:在軸變化之前刪除排列的視圖,並在軸變化之后重新添加它們。 這行得通,但也很棘手,對於某些復雜的情況在實踐中可能很困難。
  • 解決方法 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的優先級,以允許自動布局暫時打破約束而無需抱怨。

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

我不知道改變優先級是否比改變激活更有效或更不有效? 但可能是需要考慮的事情。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM