Why isn't UIButton returning correct constraints?
Asked Answered
B

2

9

In my code below: I have 5 buttons added into a vertical scrollView. Each button is constrained to the scrollViews's top + 20 ,leading, trailing edges and its height. I have created a b1HeightConstraint variable. It's there to hold the heightConstraint of the b1 button.

In a button click, I'm trying to remove this constraint. Yet I'm facing an odd issue:

When I log the constraints I only see 2 constraints, even though I've added 4 constraints to it. My the view debug hierarchy is like below:

enter image description here

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

The code is ready to just be dumpped in the ViewController class. Works out of the box. My code is a spinoff of the code written here

Bolter answered 10/8, 2018 at 20:6 Comment(3)
To summarize answers: 1) The grayed out constraints are active yet 'affectless' due to having a lower priority 2) The constraint is created because iOS adds constraints to match the intrinsicContentSize of buttons/labels/texfield/textviews (if it was a plain UIView then there would no constraints added, because it has no intrinsicContentSize 3) When you activate a constraint, a constraint is added (by iOS) to the most common ancestor of the two items mentioned in the constraint. Hence you don't see the constraints added to the button itself. They get added to its parent (scrollView)Bolter
4) Additionally on Xcode 8.3.3 the grayed constraints are showing incorrect priority. It's not 1000. It's an adaptive priority of: Hug:250 CompressionResistance:750 ie 250 when stretched and 750 when compressed. For newer versions @1000 is not appearing anymore.Bolter
FWIW if you have UIViewAlertForUnsatisfiableConstraints then any constraint that is broken will become inactive ie grayed outBolter
C
7

Here are several comments about your code:

  1. You never added any constraints to any views, so you shouldn't be removing them. iOS (CocoaTouch) added those constraints to those views, so please don't touch them. (In other words: don't call removeConstraint when you didn't call addConstraint). Your control over constraints is activating and deactivating them. Leave the adding and removing to iOS.
  2. When you activate a constraint, a constraint is added (by iOS) to the most common ancestor of the two items mentioned in the constraint. So if the two views are siblings, it will be added to the parent. If the two views are parent and child, the constraint will be added to the parent. If the two views are grandparent and grandchild, it will be added to the grandparent. If the two views are first cousins, the constraint will be added to their common grandparent.
  3. These lines of code:

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

    are creating a new constraint and assigning it to b1HeightConstraint, but you never activated this constraint, so it hasn't have been added to any view at all. So trying to remove it was never going to work, because that constraint exists only in your b1HeightConstraint property. Since it was never activated, it isn't actually constraining anything.

  4. If you want to shrink a button, you need to do one of these: a) modify the constant property of its height constraint OR b) set its height constraint's isActive property to false and then give it a new height constraint OR c) modify the priorities of the active constraints to have Auto Layout choose to use different constraints.

  5. In your view debug hierarchy, all the constraints shown are active constraints (meaning they are available to be used by Auto Layout). The grayed out ones are the ones Auto Layout chose not to use because a higher priority constraint had precedence over it. This causes no conflict. The self.height = 34 (content size) constraint is added by the system to account for content compression and content hugging. UIButtons resist compression with priority 750 and resist expansion with priority 250. The self.height = 34 (content size) constraint is grayed out because content hugging has a priority of 250 and another higher priority constraint was used instead (the constraint which sets the button's height equal to the scrollView's height has priority 1000).


Updated Code:

Here is your modified code. I changed two things:

  1. I made sure b1HeightConstraint was an activated constraint.
  2. I changed the shrink method to deactivate the old height constraint and then create and activate a new one.

Updated code

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

Options for shrinking the button's height

  1. Setting the constant property of the height constraint

    // shrink button's height by 200 points
    b1HeightConstraint?.constant -= 200
    
  2. Deactivate the old constraint and create and activate a new one

    // make button height 20 points
    b1HeightConstraint?.isActive = false
    b1HeightConstraint = sender.heightAnchor.constraint(equalToConstant: 20)
    b1HeightConstraint?.isActive = true
    
  3. Change the priority of the height constraint

    // 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)
    

    Note: For this to work, you have to give the height constraint an initial priority less than 1000 (999 works nicely) because Auto Layout will not let you change the priority of an active constraint if it is required (priority 1000).

    OR

    // 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
    
Cheater answered 11/8, 2018 at 9:47 Comment(18)
You never added any constraints to any views, so you shouldn't be removing them then why in my screenshot that view has 4 + 3 constraints?Bolter
Next sentence: iOS (CocoaTouch) added those constraints to those views. My point is, don't call removeConstraint if you didn't call addConstraint.Cheater
right right. iOS added the 3. The 4 I had originally were added to the parent. In my screenshot are the grayed out constraints the inactive constraints? The reason I'm still thinking about this is why aren't there any conflicts between these 7 constraints. one constraint is restricting the width to 31 another is restricting it to the view's width...Bolter
after adding an additional explicit height constraint I logged the constraints on the button. 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)> hence 1: the grayed out constraints are active!Bolter
2. notice the that it's not NSLayoutConstraint , it's NSContentSizeLayoutConstraint. I'm looking up their difference now. It's like have to do something with its intrinsicSize...Bolter
So here's what I think is happening. Those extra constraints are added by the system for content hugging. They have a lower priority, so they only act if a higher priority constraint isn't overriding their behavior. In this case, the grayed out constraints are not actively contributing to the layout because a higher priority constraint was used.Cheater
why are you saying the priority is lower? Aren't they all set to 1000? I'm guessing for the same priority: NSContentSizeLayoutConstraint has a lower priority compared to NSLayoutConstraint. Or did I just say exactly what you meant just differently?Bolter
See the values after CompressionResistance? Those are the priorities. So if you were to try to set a button width with a priority less than 750, the compressionResistance would say, hold on a minute, I have a priority of 750 and I say the button can't be less than 43 wide.Cheater
Likewise, if you were to use a constraint with a priority less than 250 and try to make the button wide, the Hug would say, hold on a minute, I have a priority of 250 and I say the button width can't be larger than 43.Cheater
I was talking about the constraints in the screenshot. self.width = 31 @ 1000 (content size) Why doesn't that generate any conflicts with the 4 constraints set onto its parent?Bolter
I'm not sure. If they were active constraints, they would show up in the button's constraints when you printed them because they do not reference any other view. The (content Size) does imply it is about constraining the button to the size of the content, which is what the hugging and compression resistance are about. I think it's just misleading output (the @1000) The NSContentSizeLayoutConstraint uses the Hug and CompressionResistance priorities to decide if it is actually constraining the view.Cheater
Eureka! I added btnHeight.priority = UILayoutPriority(rawValue: 100) to my code above before activating btnHeight and the grayed out constraints changed. My 100 priority constraint was grayed out, because being a lower priority constraint, it didn't contribute to the layout. Instead, the system chose self.height = 34 (content size) which came from the Hug.Cheater
To summarize, all the constraints shown are active. The grayed out ones are the ones Auto Layout isn't using because a higher priority constraint had precedence over it. The self.height = 34 (content size) constraint is added by the system to account for content compression and content hugging. It is grayed out because content compression has a priority of 750 and content hugging has a priority of 250 and other higher priority constraints were used.Cheater
Let us continue this discussion in chat.Bolter
My pleasure boss. I got most of it back in this question itself. It already helped me answer (+accepted) another question + made me 2X more comfortable using view-debugger and smarter when it comes to 'how autolayout works'. I got more out of this question than you didBolter
I finally tried what you said in the chatroom :/. It didn't crash. Just that the result was weird. Something I can't really explain. b1HeightConstraint?.isActive = false; b1HeightConstraint?.constant = 100; b1HeightConstraint?.isActive = true; isSrinked = trueBolter
Hi boss. If you ever had the time. Can you take a look hereBolter
FWIW if you have UIViewAlertForUnsatisfiableConstraints then any constraint that is broken will become inactive ie grayed out...Bolter
A
1

This is because a constraint between a view and its superView is added to the superView , you only see height/width constraint if they are static added to the UIButton , look to this diagram from Vandad IOS Book

enter image description here

see this Demo

Augend answered 10/8, 2018 at 20:9 Comment(4)
A) If I added a constraint between button3 & button4 would it get added to subview2 or view? My guess is subview2 B) Additionally shouldn't the button's width and height be equal to the scrollView? And if the button's width is 31 then it shouldn't get stretched across the entire screen's widthBolter
To subview2 , scrollView gets it's contentSize from its subviews , so you should hook the leading and trailing , and attach the width of th inner view from outside say to be equal to self.view that's how the scrollView knows it's width, regarding to height from hooking elements properly from top of the scrollview to it's bottomAugend
(This project was just for testing something). My hierarchy is like: View -> ScrollView -> ContainerView -> Button. I want the buttons to be aligned to the left and right of the screen. Are you saying to do such I need to constrain the leading/trailing the view?Bolter
I wasn't able to open the file. Nonetheless when I constrain the buttons to the leading/trailing to either the scrollView/view, still when I print the button's constraints I only see the width and height as mentioned in the question. Yet those are not being applied at all, because the button is correctly touching the leading/trailing edges of the screen, meaning the button's width is not 31, nor its height is 34. So I'm still confused as to 1. where the constraints come from? The reason I'm asking all this is because I want to control the height of the button.Bolter

© 2022 - 2024 — McMap. All rights reserved.