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จะทำงานได้อีกสองทางเลือก ...

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

ฉันไม่รู้ว่าการเปลี่ยนลำดับความสำคัญจะมีประสิทธิภาพมากกว่าหรือน้อยกว่าการเปลี่ยนการเปิดใช้งานหรือไม่? แต่อาจเป็นสิ่งที่ต้องพิจารณา