Почему UIButton не возвращает правильные ограничения?

В моем коде ниже: у меня есть 5 кнопок, добавленных в вертикальный scrollView. Каждая кнопка ограничена верхними + 20 элементами scrollViews, передними и задними краями и их высотой. Я создал переменную b1HeightConstraint. Он предназначен для удержания heightConstraint кнопки b1.

Одним нажатием кнопки я пытаюсь удалить это ограничение. Тем не менее, я столкнулся со странной проблемой:

Когда я регистрирую ограничения, я вижу только 2 ограничения, хотя я добавил к нему 4 ограничения. Моя иерархия отладки представления выглядит следующим образом:

введите описание изображения здесь

import UIKit
import Foundation

class ViewController: UIViewController {
    var filterView: UIView!
    var scrollView: UIScrollView!
    var containerView: UIView!

    override func loadView() {
        filterView = UIView()
        view = filterView
        view.backgroundColor = #colorLiteral(red: 0.909803926944733, green: 0.47843137383461, blue: 0.643137276172638, alpha: 1.0)

        scrollView = UIScrollView()
        scrollView.backgroundColor = #colorLiteral(red: 0.474509805440903, green: 0.839215695858002, blue: 0.976470589637756, alpha: 1.0)
        view.addSubview(scrollView)
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
        scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
        scrollView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 1).isActive = true
        scrollView.isScrollEnabled = true

        containerView = UIView()
        containerView.backgroundColor = #colorLiteral(red: 0.176470592617989, green: 0.498039215803146, blue: 0.756862759590149, alpha: 1.0)
        scrollView.addSubview(containerView)
        containerView.translatesAutoresizingMaskIntoConstraints = false

        // This is key:  connect all four edges of the containerView to
        // to the edges of the scrollView
        containerView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
        containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
        containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
        containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true

        // Making containerView and scrollView the same height means the
        // content will not scroll vertically
        containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
    }



    let b1 = Buttons(titleText: "one")
    let b2 = Buttons(titleText: "two")
    let b3 = Buttons(titleText: "three")
    let b4 = Buttons(titleText: "four")
    let b5 = Buttons(titleText: "five")
    var b1HeightConstraint : NSLayoutConstraint?

    override func viewDidLoad() {
        super.viewDidLoad()


        let buttonArray = [b1, b2, b3, b4, b5]

        b1.button.addTarget(self, action: #selector(ViewController.shrink(_:)), for: .touchUpInside)

        var startPoint = containerView.topAnchor

        for btn in buttonArray {
            let theBtn = btn.button
            containerView.addSubview(theBtn)
            theBtn.translatesAutoresizingMaskIntoConstraints = false
            theBtn.topAnchor.constraint(equalTo: startPoint, constant: 20).isActive = true
            theBtn.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
            theBtn.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
            theBtn.heightAnchor.constraint(equalTo: scrollView.heightAnchor).isActive = true

            startPoint = theBtn.bottomAnchor
            let btnHeight = theBtn.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
            if btn == b1{
                b1HeightConstraint = btnHeight
            }
        }

        containerView.bottomAnchor.constraint(equalTo: startPoint, constant: 20).isActive = true

    }

    @objc func shrink(_ sender: Any){
        guard let btn = sender as? UIButton else{
            return
        }
        print("count is: \(btn.constraints.count)")

        btn.removeConstraint(b1HeightConstraint!)
        containerView.removeConstraint(b1HeightConstraint!)
        print("count is: \(btn.constraints.count)")
        containerView.updateConstraintsIfNeeded()
        containerView.updateConstraints()
        scrollView.updateConstraintsIfNeeded()
        scrollView.updateConstraints()
    }
}

class Buttons : NSObject {
    let button = UIButton()
    init(titleText: String) {
        button.backgroundColor = #colorLiteral(red: 0.976470589637756, green: 0.850980401039124, blue: 0.549019634723663, alpha: 1.0)
        button.setTitle(titleText, for: .normal)
    }
}

Код готов, его можно просто сбросить в класс ViewController. Работает из коробки. Мой код является побочным продуктом кода, написанного здесь


person Honey    schedule 10.08.2018    source источник
comment
Подводя итог ответам: 1) Ограничения, выделенные серым цветом, активны, но «не действуют» из-за более низкого приоритета 2) Ограничение создается, потому что iOS добавляет ограничения, соответствующие внутреннему размеру содержимого. кнопок/меток/текстовых полей/текстовых представлений (если бы это был простой UIView, тогда не было бы добавленных ограничений, потому что у него нет intrinsicContentSize 3) Когда вы активируете ограничение, ограничение добавляется (IOS) к наиболее общему предку два элемента, упомянутых в ограничении. Следовательно, вы не видите ограничений, добавленных к самой кнопке. Они добавляются к его родителю (scrollView)   -  person Honey    schedule 11.08.2018
comment
4) Кроме того, в Xcode 8.3.3 серые ограничения показывают неправильный приоритет. Это не 1000. Это адаптивный приоритет: Hug:250 CompressionResistance:750 т.е. 250 при растяжении и 750 при сжатии. В более новых версиях @1000 больше не появляется.   -  person Honey    schedule 13.08.2018
comment
FWIW, если у вас есть UIViewAlertForUnsatisfiableConstraints, то любое нарушенное ограничение станет неактивным, т.е. будет выделено серым цветом.   -  person Honey    schedule 03.09.2019


Ответы (2)


Вот несколько комментариев о вашем коде:

  1. Вы никогда не добавляли никаких ограничений ни к каким представлениям, поэтому вам не следует их удалять. iOS (CocoaTouch) добавила эти ограничения в эти представления, поэтому не трогайте их. (Другими словами: не звоните removeConstraint, если вы не звонили addConstraint). Вы управляете ограничениями, активируя и деактивируя их. Оставьте добавление и удаление на iOS.
  2. Когда вы активируете ограничение, ограничение добавляется (IOS) к наиболее общему предку двух элементов, упомянутых в ограничении. Поэтому, если два представления являются одноуровневыми, они будут добавлены к родительскому. Если два представления являются родительским и дочерним, ограничение будет добавлено к родительскому. Если два представления относятся к прародителю и внуку, оно будет добавлено к прародителю. Если два представления являются двоюродными братьями и сестрами, ограничение будет добавлено к их общему прародителю.
  3. Эти строки кода:

    let btnHeight = theBtn.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
    if btn == b1{
        b1HeightConstraint = btnHeight
    }
    

    создают новое ограничение и назначают его b1HeightConstraint, но вы никогда не активировали это ограничение, поэтому оно вообще не было добавлено ни в какое представление. Поэтому попытка удалить его никогда не сработает, потому что это ограничение существует только в вашем свойстве b1HeightConstraint. Поскольку он никогда не был активирован, он фактически ничего не ограничивает.

  4. Если вы хотите уменьшить кнопку, вам нужно выполнить одно из следующих действий: а) изменить свойство constant ее ограничения по высоте ИЛИ б) установить для свойства isActive ее ограничения по высоте значение false, а затем присвоить ей новое значение ограничение по высоте ИЛИ c) изменить приоритеты активных ограничений, чтобы Auto Layout выбрал использование других ограничений.

  5. В иерархии отладки вашего представления все показанные ограничения являются активными ограничениями (это означает, что они доступны для использования Auto Layout). Серые — это те, которые Auto Layout решил не использовать, потому что ограничение с более высоким приоритетом имело приоритет над ним. Это не вызывает конфликтов. Ограничение self.height = 34 (content size) добавляется системой для учета сжатия контента и объема контента. UIButtons сопротивляются сжатию с приоритетом 750 и сопротивляются расширению с приоритетом 250. Ограничение self.height = 34 (content size) неактивно, потому что обхват содержимого имеет приоритет 250, и вместо него было использовано другое ограничение с более высоким приоритетом (ограничение, которое устанавливает высоту кнопки равной высоте scrollView, имеет приоритет 1000).


Обновлен код:

Вот ваш измененный код. Я изменил две вещи:

  1. Я убедился, что b1HeightConstraint было активированным ограничением.
  2. Я изменил метод shrink, чтобы деактивировать старое ограничение по высоте, а затем создать и активировать новое.

Обновленный код

import UIKit
import Foundation

class ViewController: UIViewController {
    var filterView: UIView!
    var scrollView: UIScrollView!
    var containerView: UIView!

    override func loadView() {
        filterView = UIView()
        view = filterView
        view.backgroundColor = #colorLiteral(red: 0.909803926944733, green: 0.47843137383461, blue: 0.643137276172638, alpha: 1.0)

        scrollView = UIScrollView()
        scrollView.backgroundColor = #colorLiteral(red: 0.474509805440903, green: 0.839215695858002, blue: 0.976470589637756, alpha: 1.0)
        view.addSubview(scrollView)
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
        scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
        scrollView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 1).isActive = true
        scrollView.isScrollEnabled = true

        containerView = UIView()
        containerView.backgroundColor = #colorLiteral(red: 0.176470592617989, green: 0.498039215803146, blue: 0.756862759590149, alpha: 1.0)
        scrollView.addSubview(containerView)
        containerView.translatesAutoresizingMaskIntoConstraints = false

        // This is key:  connect all four edges of the containerView to
        // to the edges of the scrollView
        containerView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
        containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
        containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
        containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true

        // Making containerView and scrollView the same height means the
        // content will not scroll vertically
        containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
    }

    let b1 = Buttons(titleText: "one")
    let b2 = Buttons(titleText: "two")
    let b3 = Buttons(titleText: "three")
    let b4 = Buttons(titleText: "four")
    let b5 = Buttons(titleText: "five")
    var b1HeightConstraint : NSLayoutConstraint?

    override func viewDidLoad() {
        super.viewDidLoad()

        let buttonArray = [b1, b2, b3, b4, b5]

        b1.button.addTarget(self, action: #selector(ViewController.shrink(_:)), for: .touchUpInside)

        var startPoint = containerView.topAnchor

        for btn in buttonArray {
            let theBtn = btn.button
            containerView.addSubview(theBtn)
            theBtn.translatesAutoresizingMaskIntoConstraints = false
            theBtn.topAnchor.constraint(equalTo: startPoint, constant: 20).isActive = true
            theBtn.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
            theBtn.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
            //theBtn.heightAnchor.constraint(equalTo: scrollView.heightAnchor).isActive = true

            startPoint = theBtn.bottomAnchor
            let btnHeight = theBtn.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
            btnHeight.isActive = true
            if btn == b1{
                b1HeightConstraint = btnHeight
            }
        }

        containerView.bottomAnchor.constraint(equalTo: startPoint, constant: 20).isActive = true

    }

    @objc func shrink(_ sender: UIButton) {
        b1HeightConstraint?.isActive = false
        b1HeightConstraint = sender.heightAnchor.constraint(equalToConstant: 20)
        b1HeightConstraint?.isActive = true
    }
}

class Buttons : NSObject {
    let button = UIButton()
    init(titleText: String) {
        button.backgroundColor = #colorLiteral(red: 0.976470589637756, green: 0.850980401039124, blue: 0.549019634723663, alpha: 1.0)
        button.setTitle(titleText, for: .normal)
    }
}

Варианты уменьшения высоты кнопки

  1. Настройка свойства constant ограничения по высоте

    // shrink button's height by 200 points
    b1HeightConstraint?.constant -= 200
    
  2. Отключить старое ограничение, создать и активировать новое

    // make button height 20 points
    b1HeightConstraint?.isActive = false
    b1HeightConstraint = sender.heightAnchor.constraint(equalToConstant: 20)
    b1HeightConstraint?.isActive = true
    
  3. Изменить приоритет ограничения высоты

    // Set b1HeightConstraint's priority to less than 250, and the
    // *content hugging* with priority 250 will take over and resize
    // the button to its intrinsic height
    b1HeightConstraint?.priority = UILayoutPriority(rawValue: 100)
    

    Примечание. Чтобы это работало, вы должны задать ограничению высоты начальный приоритет меньше 1000 (999 прекрасно работает), потому что Auto Layout не позволит вам изменить приоритет активного ограничения, если это необходимо. (приоритет 1000).

    ИЛИ

    // Just deactivate the buttonHeight constraint and the *content
    // hugging* will take over and it will set the button's height
    // to its intrinsic height
    b1HeightConstraint?.isActive = false
    
person vacawama    schedule 11.08.2018
comment
Вы никогда не добавляли никаких ограничений ни к каким представлениям, поэтому вам не следует их удалять тогда почему на моем снимке экрана у этого представления 4 + 3 ограничения? - person Honey; 11.08.2018
comment
Следующее предложение: iOS (CocoaTouch) добавила эти ограничения в эти представления. Я хочу сказать, не звоните removeConstraint, если вы не звонили addConstraint. - person vacawama; 11.08.2018
comment
верно-верно. iOS добавила 3. 4, которые у меня были изначально, были добавлены к родителю. На моем снимке экрана выделенные серым цветом ограничения являются неактивными ограничениями? Причина, по которой я все еще думаю об этом, заключается в том, почему между этими 7 ограничениями нет конфликтов. одно ограничение ограничивает ширину до 31, другое ограничивает ширину представления... - person Honey; 11.08.2018
comment
после добавления дополнительного явного ограничения высоты я записал ограничения на кнопку. po b3.button.constraints ▿ 3 elements - 0 : <NSContentSizeLayoutConstraint:0x6080000b0c20 UIButton:0x7f8c3270a370'three'.width == 43 Hug:250 CompressionResistance:750 (active)> - 1 : <NSContentSizeLayoutConstraint:0x6080000b0c80 UIButton:0x7f8c3270a370'three'.height == 34 Hug:250 CompressionResistance:750 (active)> - 2 : <NSLayoutConstraint:0x600000096a80 UIButton:0x7f8c3270a370'three'.height == 200 (active)> отсюда 1: выделенные серым цветом ограничения активны! - person Honey; 11.08.2018
comment
2. обратите внимание, что это не NSLayoutConstraint, это NSContentSizeLayoutConstraint. Сейчас ищу их разницу. Это похоже на то, что нужно что-то делать с его внутренним размером... - person Honey; 11.08.2018
comment
Итак, вот что, я думаю, происходит. Эти дополнительные ограничения добавляются системой для захвата контента. У них более низкий приоритет, поэтому они действуют только в том случае, если ограничение с более высоким приоритетом не переопределяет их поведение. В этом случае ограничения, выделенные серым цветом, не вносят активного вклада в компоновку, поскольку использовалось ограничение с более высоким приоритетом. - person vacawama; 11.08.2018
comment
почему вы говорите, что приоритет ниже? Разве они не установлены на 1000? Я предполагаю, что для того же приоритета: NSContentSizeLayoutConstraint имеет более низкий приоритет по сравнению с NSLayoutConstraint. Или я просто сказал именно то, что вы имели в виду, просто по-другому? - person Honey; 11.08.2018
comment
Видите значения после CompressionResistance? Таковы приоритеты. Поэтому, если вы попытаетесь установить ширину кнопки с приоритетом меньше 750, CompressionResistance скажет: подождите минутку, у меня приоритет 750, и я говорю, что ширина кнопки не может быть меньше 43. - person vacawama; 11.08.2018
comment
Точно так же, если бы вы использовали ограничение с приоритетом меньше 250 и попытались сделать кнопку широкой, Hug сказал бы: подождите минутку, у меня приоритет 250, и я говорю, что ширина кнопки не может быть больше. чем 43. - person vacawama; 11.08.2018
comment
Я говорил об ограничениях на скриншоте. self.width = 31 @ 1000 (content size) Почему это не создает никаких конфликтов с 4 ограничениями, установленными для его родителя? - person Honey; 11.08.2018
comment
Я не уверен. Если бы они были активными ограничениями, они бы отображались в ограничениях кнопки при их печати, потому что они не ссылаются ни на какое другое представление. (content Size) подразумевает, что речь идет об ограничении кнопки размером содержимого, что и касается сопротивления обниманию и сжатию. Я думаю, что это просто вводящий в заблуждение вывод (the @1000) NSContentSizeLayoutConstraint использует приоритеты Hug и CompressionResistance, чтобы решить, действительно ли он ограничивает представление. - person vacawama; 11.08.2018
comment
Эврика! Я добавил btnHeight.priority = UILayoutPriority(rawValue: 100) в свой код выше, прежде чем активировать btnHeight, и ограничения, выделенные серым цветом, изменились. Мое ограничение приоритета 100 было выделено серым цветом, потому что, будучи ограничением с более низким приоритетом, оно не влияло на макет. Вместо этого система выбрала self.height = 34 (content size) из Hug. - person vacawama; 11.08.2018
comment
Подводя итог, можно сказать, что все показанные ограничения активны. Затененные — это те, которые Auto Layout не использует, поскольку ограничение с более высоким приоритетом имеет приоритет над ним. Ограничение self.height = 34 (content size) добавляется системой для учета сжатия контента и объема контента. Он неактивен, потому что сжатие контента имеет приоритет 750, а сжатие контента имеет приоритет 250, и использовались другие ограничения с более высоким приоритетом. - person vacawama; 11.08.2018
comment
Давайте продолжим обсуждение в чате. - person Honey; 11.08.2018
comment
Мой босс удовольствия. Я получил большую часть этого в самом этом вопросе. Это уже помогло мне ответить (+принято) на другой вопрос + сделало меня в 2 раза более удобным при использовании отладчика представления и умнее, когда дело доходит до того, «как работает автомакет». Я получил больше из этого вопроса, чем вы - person Honey; 10.09.2018
comment
Наконец-то я попробовал то, что вы сказали в чате :/. Он не разбился. Вот только результат был странным. Что-то я не могу толком объяснить. b1HeightConstraint?.isActive = false; b1HeightConstraint?.constant = 100; b1HeightConstraint?.isActive = true; isSrinked = true - person Honey; 07.10.2018
comment
Привет, босс. Если у вас когда-нибудь было время. Вы можете посмотреть здесь - person Honey; 08.10.2018
comment
FWIW, если у вас есть UIViewAlertForUnsatisfiableConstraints, то любое нарушенное ограничение станет неактивным, т.е. будет выделено серым цветом... - person Honey; 03.09.2019

Это связано с тем, что ограничение между представлением и его superView добавляется в superView , вы видите ограничение высоты/ширины только в том случае, если они статически добавлены к UIButton , посмотрите на эту диаграмму из книги Vandad IOS.

введите здесь описание изображения

см. эту демонстрацию

person Sh_Khan    schedule 10.08.2018
comment
A) Если я добавлю ограничение между button3 и button4, будет ли оно добавлено в subview2 или представление? Я предполагаю, что это subview2 B) Кроме того, не должны ли ширина и высота кнопки быть равными scrollView? И если ширина кнопки 31, то она не должна растягиваться на всю ширину экрана. - person Honey; 10.08.2018
comment
Для subview2 scrollView получает свой contentSize из своих подпредставлений, поэтому вы должны подключить начальное и конечное, и прикрепить ширину внутреннего представления извне, скажем, равную self.view, как это знает scrollView это ширина, относительно высоты от правильного закрепления элементов от верхней части прокрутки до нижней части - person Sh_Khan; 10.08.2018
comment
(Этот проект был просто для тестирования чего-то). Моя иерархия выглядит так: View -> ScrollView -> ContainerView -> Button. Я хочу, чтобы кнопки были выровнены по левому и правому краям экрана. Вы говорите, что для этого мне нужно ограничить начало/конец представления? - person Honey; 10.08.2018
comment
Я не смог открыть файл. Тем не менее, когда я ограничиваю кнопки ведущим/завершающим либо прокруткой/представлением, тем не менее, когда я печатаю ограничения кнопки, я вижу только ширину и высоту, как указано в вопросе. Тем не менее, они вообще не применяются, потому что кнопка правильно касается переднего/заднего края экрана, что означает, что ширина кнопки не равна 31, а ее высота не равна 34. Итак, я все еще не понимаю, 1. откуда берутся ограничения? Причина, по которой я все это спрашиваю, заключается в том, что я хочу контролировать высоту кнопки. - person Honey; 11.08.2018