ajay_sarkate
ajay_sarkate

Reputation: 31

Adding Constraints to view, whose topAnchor's is bottomAnchor of UIView with dynamic height

I've created a UIView, which has buttons in it. I set the constraints like this :

NSLayoutConstraint.activate([
    sampleView.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: 20),
    sampleView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
    sampleView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
])

Not added any height or bottom anchor as I want it to be dynamic in height. Initially sampleView has no buttons, so it doesn't appear in UI. I then add buttons in it vertical to each other and it works fine.

The problem I'm facing right now is, there is another view(tempView) just below this sampleView. And initially I set tempView's topAnchor to sampleView's bottomAnchor. So it is placed properly on screen initially.

When I add buttons in sampleView, it expands, but tempView don't. I tried with view.layoutIfNeeded() and setting the topAnchor with new height of sampleView but nothing works out for me.

Upvotes: 0

Views: 259

Answers (1)

DonMag
DonMag

Reputation: 77672

You must do something to give sampleView a Height.

One easy way is to add a vertical UIStackView as a subview of sampleView, constrain all 4 sides to sampleView, and then add buttons as arranged subviews of the stack view.

Quick example:

class ExampleVC: UIViewController {
    
    let topView = UIView()
    let sampleView = UIView()
    let tempView = UIView()
    
    // let's use a vertical stack view to hold the buttons inside sampleView
    let stackView = UIStackView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        
        [topView, sampleView, tempView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
        }
        
        topView.backgroundColor = .yellow
        sampleView.backgroundColor = .green
        tempView.backgroundColor = .cyan
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            topView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            topView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
            topView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
            topView.heightAnchor.constraint(equalToConstant: 100.0),
            
            sampleView.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: 20.0),
            sampleView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
            sampleView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
            // sampleView has no Height constraint ...
            //  we're adding a stack view to it, and as we add buttons to the stack view
            //  it will control the Height
            
            tempView.topAnchor.constraint(equalTo: sampleView.bottomAnchor, constant: 20.0),
            tempView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
            tempView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
            tempView.heightAnchor.constraint(equalToConstant: 100.0),
            
        ])
        
        // stackView to hold the buttons
        stackView.axis = .vertical
        stackView.spacing = 8
        
        stackView.translatesAutoresizingMaskIntoConstraints = false
        sampleView.addSubview(stackView)
        
        NSLayoutConstraint.activate([
            
            // constrain all 4 sides of the stack view to sampleView
            stackView.topAnchor.constraint(equalTo: sampleView.topAnchor, constant: 0.0),
            stackView.leadingAnchor.constraint(equalTo: sampleView.leadingAnchor, constant: 0.0),
            stackView.trailingAnchor.constraint(equalTo: sampleView.trailingAnchor, constant: 0.0),
            stackView.bottomAnchor.constraint(equalTo: sampleView.bottomAnchor, constant: 0.0),
            
        ])
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let btnNumber = stackView.arrangedSubviews.count + 1
        let btn = UIButton()
        btn.backgroundColor = .red
        btn.setTitle("Button \(btnNumber)", for: [])
        stackView.addArrangedSubview(btn)
    }
    
}

At start, because we haven't added any buttons to the stack view, it will look like this:

enter image description here

Tap anywhere to add buttons -- here is what we get after the first tap:

enter image description here

and after a couple more taps:

enter image description here


Edit - quick example #2...

You didn't include any information about how you are arranging the buttons horizontal to each other if fitted in one line else new button will down to previous -- but here is one approach.

Add a variable Height constraint as a property to your controller class:

// this will be updated when we add buttons
var sampleViewHeightConstraint: NSLayoutConstraint!

We will initialize sampleViewHeightConstraint as the .heightAnchor of sampleView, with a constant of Zero.

When we add button(s) to sampleView, we will calculate where the new button should be placed (i.e. "wrapping to the next row" if we've reached the edge of the view).

We will then update sampleViewHeightConstraint.constant based on the bottom of the last button added:

class Example2VC: UIViewController {
    
    let topView = UIView()
    let sampleView = UIView()
    let tempView = UIView()

    // this will be updated when we add buttons
    var sampleViewHeightConstraint: NSLayoutConstraint!
    
    let btnTitles: [String] = [
        "here", "are some", "sample", "button", "titles",
        "so we have", "varying width", "buttons",
        "that will be arranged", "on rows fitting",
        "inside", "this view",
    ]

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        
        [topView, sampleView, tempView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
        }
        
        topView.backgroundColor = .yellow
        sampleView.backgroundColor = .green
        tempView.backgroundColor = .cyan
        
        let g = view.safeAreaLayoutGuide
        
        // initialize the sampleViewHeightConstraint
        sampleViewHeightConstraint = sampleView.heightAnchor.constraint(equalToConstant: 0.0)
        
        NSLayoutConstraint.activate([
            
            topView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            topView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
            topView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
            topView.heightAnchor.constraint(equalToConstant: 100.0),
            
            sampleView.topAnchor.constraint(equalTo: topView.bottomAnchor, constant: 20.0),
            sampleView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
            sampleView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
            // sampleViewHeightConstraint will be updated when we add buttons
            sampleViewHeightConstraint,
            
            tempView.topAnchor.constraint(equalTo: sampleView.bottomAnchor, constant: 20.0),
            tempView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 16.0),
            tempView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -16.0),
            tempView.heightAnchor.constraint(equalToConstant: 100.0),
            
        ])
        
    }
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        
        let thisTitle: String = btnTitles[sampleView.subviews.count % btnTitles.count]

        let btn = UIButton()
        btn.backgroundColor = .red
        btn.setTitle(thisTitle, for: [])
        
        let sz: CGSize = btn.intrinsicContentSize
        // let's make the button frame a little wider than the title
        btn.frame = .init(x: 0.0, y: 0.0, width: sz.width + 16.0, height: sz.height)

        if sampleView.subviews.count == 0 {
            // this is the first button we're adding,
            //  so put it at 8,8
            btn.frame.origin = .init(x: 8.0, y: 8.0)
        } else {
            if let lastBtn = sampleView.subviews.last {
                btn.frame.origin = .init(x: lastBtn.frame.maxX + 8.0, y: lastBtn.frame.minY)
                if btn.frame.maxX > sampleView.frame.width - 8.0 {
                    // extends past the right edge, so "move it down a row"
                    btn.frame.origin = .init(x: 8.0, y: lastBtn.frame.maxY + 8.0)
                }
            }
        }

        // add the new button
        sampleView.addSubview(btn)
        
        // update the sampleViewHeightConstraint
        if let lastBtn = sampleView.subviews.last {
            sampleViewHeightConstraint.constant = lastBtn.frame.maxY + 8.0
        }
        
    }
    
}

as with the first example, it starts out like this:

enter image description here

we tap anywhere to add a button:

enter image description here

tap a few times to add more buttons:

enter image description here

the next button will exceed the view width, so we "wrap" it:

enter image description here

another tap:

enter image description here

and after a bunch of taps:

enter image description here

However you are adding the buttons and arranging them, the key point is that you must do something to give sampleView a height... updating the .constant value of a constraint as shown in this example is one way to do so.

Upvotes: 1

Related Questions