0
votes

I have defined a view to be a pop-up on an iOS screen using programmed constraints.

    let stopTimer = StoppageTimer(frame: CGRect.zero)

The view itself contains a stack view, plus a couple of buttons. When I try to set constraints for my view (from its superview - a View Controller), all of them are applied correctly except the height of my view. The code that sets these constraints is (the offending set are the last four, just before view.layoutIfNeeded()

func setConstraints() {
    // Remove all constraints within the UIView
    view.constraints.forEach {constraint in constraint.isActive = false}
    lblNetScore.translatesAutoresizingMaskIntoConstraints = false
    lblMatchName.translatesAutoresizingMaskIntoConstraints = false
    butUnwind.translatesAutoresizingMaskIntoConstraints = false
    butMatchStats.translatesAutoresizingMaskIntoConstraints = false
    GSButtons.translatesAutoresizingMaskIntoConstraints = false
    GAButtons.translatesAutoresizingMaskIntoConstraints = false
    sb.translatesAutoresizingMaskIntoConstraints = false
    timer.translatesAutoresizingMaskIntoConstraints = false
    butSwitch.translatesAutoresizingMaskIntoConstraints = false
    Qtr.translatesAutoresizingMaskIntoConstraints = false
    butStart.translatesAutoresizingMaskIntoConstraints = false
    stopTimer.translatesAutoresizingMaskIntoConstraints = false
    // Top Line
    NSLayoutConstraint(item: butUnwind,     attribute: .leading,  relatedBy: .equal, toItem: view, attribute: .leading,    multiplier: 1, constant:  15).isActive = true
    NSLayoutConstraint(item: butUnwind,     attribute: .top,      relatedBy: .equal, toItem: view, attribute: .topMargin,  multiplier: 1, constant:   0).isActive = true
    NSLayoutConstraint(item: lblNetScore,   attribute: .centerX,  relatedBy: .equal, toItem: view, attribute: .centerX,    multiplier: 1, constant:   0).isActive = true
    NSLayoutConstraint(item: lblNetScore,   attribute: .top,      relatedBy: .equal, toItem: view, attribute: .topMargin,  multiplier: 1, constant:   0).isActive = true
    NSLayoutConstraint(item: butMatchStats, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing,   multiplier: 1, constant: -15).isActive = true
    NSLayoutConstraint(item: butMatchStats, attribute: .top,      relatedBy: .equal, toItem: view, attribute: .topMargin,  multiplier: 1, constant:   0).isActive = true
    NSLayoutConstraint(item: lblMatchName,  attribute: .top,      relatedBy: .equal, toItem: lblNetScore, attribute: .bottom,   multiplier: 1, constant: 5).isActive = true
    NSLayoutConstraint(item: lblMatchName,  attribute: .centerX,  relatedBy: .equal, toItem: view, attribute: .centerX,  multiplier: 1, constant:   0).isActive = true
    // Timer
    NSLayoutConstraint(item: timer, attribute: .top,      relatedBy: .equal, toItem: lblMatchName, attribute: .bottom,  multiplier: 1, constant: 5).isActive = true
    NSLayoutConstraint(item: timer, attribute: .centerX,  relatedBy: .equal, toItem: view,         attribute: .centerX, multiplier: 1, constant: 0).isActive = true

    NSLayoutConstraint(item: Qtr,   attribute: .top,      relatedBy: .equal, toItem: lblMatchName, attribute: .bottom,  multiplier: 1, constant: 5).isActive = true
    NSLayoutConstraint(item: Qtr, attribute: .leading,    relatedBy: .equal, toItem: view,         attribute: .leadingMargin, multiplier: 1, constant: 0).isActive = true
    NSLayoutConstraint(item: Qtr, attribute: .height,     relatedBy: .equal, toItem: timer,         attribute: .height, multiplier: 1, constant: 0).isActive = true
    NSLayoutConstraint(item: butStart,   attribute: .top, relatedBy: .equal, toItem: lblMatchName, attribute: .bottom,  multiplier: 1, constant: 5).isActive = true
    NSLayoutConstraint(item: butStart, attribute: .trailing,    relatedBy: .equal, toItem: view,   attribute: .trailingMargin, multiplier: 1, constant: 0).isActive = true
    NSLayoutConstraint(item: butStart, attribute: .height,     relatedBy: .equal, toItem: timer,   attribute: .height, multiplier: 1, constant: 0).isActive = true
    NSLayoutConstraint(item: butStart, attribute: .width,     relatedBy: .equal, toItem: nil,      attribute: .notAnAttribute, multiplier: 1, constant: 70).isActive = true


    // Switch Button
    NSLayoutConstraint(item: butSwitch, attribute: .top,      relatedBy: .equal, toItem: timer, attribute: .bottom,  multiplier: 1, constant: 5).isActive = true
    NSLayoutConstraint(item: butSwitch, attribute: .centerX,  relatedBy: .equal, toItem: view,  attribute: .centerX, multiplier: 1, constant: 0).isActive = true
    // ScoreBoard
    NSLayoutConstraint(item: sb, attribute: .top,      relatedBy: .equal, toItem: butSwitch, attribute: .bottom,  multiplier: 1, constant: 5).isActive = true
    NSLayoutConstraint(item: sb, attribute: .centerX,  relatedBy: .equal, toItem: view,      attribute: .centerX, multiplier: 1, constant: 0).isActive = true
    //Scoring buttons - GS
    NSLayoutConstraint(item: GSButtons, attribute: .top,      relatedBy: .equal, toItem: sb,   attribute: .bottom,        multiplier: 1, constant:   7).isActive = true
    NSLayoutConstraint(item: GSButtons, attribute: .height,   relatedBy: .equal, toItem: sb,   attribute: .height,        multiplier: 1, constant:  15).isActive = true
    NSLayoutConstraint(item: GSButtons, attribute: .leading,  relatedBy: .equal, toItem: view, attribute: .leadingMargin, multiplier: 1, constant:   0).isActive = true
    NSLayoutConstraint(item: GSButtons, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailingMargin,multiplier: 1, constant:   0).isActive = true
    // Scoring buttons - GA
    NSLayoutConstraint(item: GAButtons, attribute: .top,      relatedBy: .equal, toItem: GSButtons, attribute: .bottom,         multiplier: 1, constant:   7).isActive = true
    NSLayoutConstraint(item: GAButtons, attribute: .height,   relatedBy: .equal, toItem: sb,        attribute: .height,         multiplier: 1, constant:  15).isActive = true
    NSLayoutConstraint(item: GAButtons, attribute: .leading,  relatedBy: .equal, toItem: view,      attribute: .leadingMargin,  multiplier: 1, constant:   0).isActive = true
    NSLayoutConstraint(item: GAButtons, attribute: .trailing, relatedBy: .equal, toItem: view,      attribute: .trailingMargin, multiplier: 1, constant:   0).isActive = true
    // Stoppage Timer
    NSLayoutConstraint(item: stopTimer, attribute: .top,      relatedBy: .equal, toItem: butSwitch, attribute: .bottom,         multiplier: 1, constant: 0).isActive = true
    NSLayoutConstraint(item: stopTimer, attribute: .height,   relatedBy: .equal, toItem: nil,       attribute: .notAnAttribute, multiplier: 1, constant: 100).isActive = true
    NSLayoutConstraint(item: stopTimer, attribute: .leading,  relatedBy: .equal, toItem: view,      attribute: .leadingMargin,  multiplier: 1, constant: 0).isActive = true
    NSLayoutConstraint(item: stopTimer, attribute: .trailing, relatedBy: .equal, toItem: view,      attribute: .trailingMargin, multiplier: 1, constant: 0).isActive = true
    view.layoutIfNeeded()
}

So the view is positioned below another button, and top/leading/trailing constraints are perfect, but the height is just ignored (no constraint errors in the debug window). When I look at the height value in debug it tells me that it's zero

(lldb) po stopTimer.frame
▿ (16.0, 186.5, 343.0, 0.0)
  ▿ origin : (16.0, 186.5)
    - x : 16.0
    - y : 186.5
  ▿ size : (343.0, 0.0)
    - width : 343.0
    - height : 0.0

I declare the view up-front using CGRect.zero because my constraints will re-size later.

If I set the height to be equal to another view it works fine, but it just won't set it to be a constant height. The same thing happens with the width constraint if I try to use that in a similar way.

Any help on solving this mystery would be appreciated.

EDIT

When the stopTimer view appears (I set .isHidden = false), the controls within the subview (buttons, stack view etc.) are all shown on screen, but are inaccessible (I cannot touch on them) because they are not within the bounds of the view. Apologies for verbosity, but here is the stopTimer class definition

class StoppageTimer: UIView {

lazy var StoppageType: UISegmentedControl = {
    let s = UISegmentedControl(frame: CGRect.zero)
    s.insertSegment(withTitle: "Umpire Time", at: 0, animated: false)
    s.insertSegment(withTitle: "Injury Time", at: 1, animated: false)
    s.translatesAutoresizingMaskIntoConstraints = false
    s.backgroundColor = Style.backgroundColor
    s.tintColor = Style.buttonBackgroundColorA
    return s
}()

lazy var StoppageTimer: UIStackView = {
    let s = UIStackView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
    s.axis = .horizontal
    s.distribution = .fill
    s.alignment = .fill
    s.translatesAutoresizingMaskIntoConstraints = false
    return s
}()

let bgView: UIView = {
    let v = UIView()
    v.backgroundColor = Style.labelBackgroundColorA
    v.layer.cornerRadius = CGFloat(Style.buttonCornerRadius)
    v.layer.borderWidth = 3
    v.layer.borderColor = Style.buttonBackgroundColorA.cgColor
    v.translatesAutoresizingMaskIntoConstraints = false
    return v
}()

let minutes: UILabel = {
    let l = UILabel()
    l.text = "00"
    l.textAlignment = .right
    l.backgroundColor = UIColor.clear
    l.textColor = Style.labelTextColor
    l.font = UIFont.systemFont(ofSize: 40.0, weight: .thin)
    l.translatesAutoresizingMaskIntoConstraints = false
    return l
}()

let Separator: UILabel = {
    let l = UILabel()
    l.text = ":"
    l.textAlignment = .center
    l.backgroundColor = UIColor.clear
    l.textColor = Style.labelTextColor
    l.font = UIFont.systemFont(ofSize: 40.0, weight: .ultraLight)
    l.translatesAutoresizingMaskIntoConstraints = false
    return l
}()

let seconds: UILabel = {
    let l = UILabel()
    l.text = "00"
    l.textAlignment = .left
    l.backgroundColor = UIColor.clear
    l.textColor = Style.labelTextColor
    l.font = UIFont.systemFont(ofSize: 40.0, weight: .thin)
    l.translatesAutoresizingMaskIntoConstraints = false
    return l
}()

let butCont: UIButton = {
    let b = UIButton()
    b.setTitle("Continue", for: .normal)
    b.setTitleColor(Style.buttonTextColor, for: .normal)
    b.titleLabel?.font = UIFont.systemFont(ofSize: 25)
    b.titleLabel?.adjustsFontSizeToFitWidth = true
    b.showsTouchWhenHighlighted = true
    b.translatesAutoresizingMaskIntoConstraints = false
    b.backgroundColor = Style.buttonBackgroundColorB
    b.layer.cornerRadius = CGFloat(Style.buttonCornerRadius)
    b.layer.borderWidth = CGFloat(Style.buttonBorderWidth)
    return b
}()

override init(frame: CGRect) {
    super.init(frame: frame)
    addStoppageTimer()
}

required init(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    setStoppageTimerConstraints()
}

func addStoppageTimer() {
    StoppageTimer.arrangedSubviews.forEach { subview in subview.removeFromSuperview() }
    addSubview(bgView)
    StoppageTimer.addArrangedSubview(minutes)
    StoppageTimer.addArrangedSubview(Separator)
    StoppageTimer.addArrangedSubview(seconds)
    addSubview(StoppageTimer)
    addSubview(StoppageType)
    addSubview(butCont)
}

func setStoppageTimerConstraints() {
    constraints.forEach { constraint in constraint.isActive = false }
    translatesAutoresizingMaskIntoConstraints = false

    NSLayoutConstraint(item: bgView, attribute: .top,      relatedBy: .equal, toItem: self, attribute: .top,      multiplier: 1, constant: 0).isActive = true
    NSLayoutConstraint(item: bgView, attribute: .bottom,   relatedBy: .equal, toItem: self, attribute: .bottom,   multiplier: 1, constant: 0).isActive = true
    NSLayoutConstraint(item: bgView, attribute: .leading,  relatedBy: .equal, toItem: self, attribute: .leading,  multiplier: 1, constant: 0).isActive = true
    NSLayoutConstraint(item: bgView, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: 0).isActive = true

    NSLayoutConstraint(item: StoppageType, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 10).isActive = true
    NSLayoutConstraint(item: StoppageType, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: 20).isActive = true
    NSLayoutConstraint(item: StoppageType, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: -20).isActive = true

    NSLayoutConstraint(item: StoppageTimer, attribute: .top,      relatedBy: .equal, toItem: StoppageType, attribute: .bottom,          multiplier: 1, constant: 0).isActive = true
    NSLayoutConstraint(item: StoppageTimer, attribute: .centerX,   relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
    NSLayoutConstraint(item: StoppageTimer, attribute: .width,  relatedBy: .equal, toItem: nil, attribute: .notAnAttribute,      multiplier: 1, constant: 150).isActive = true

    NSLayoutConstraint(item: butCont, attribute: .centerX, relatedBy: .equal, toItem: bgView, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
    NSLayoutConstraint(item: butCont, attribute: .top, relatedBy: .equal, toItem: StoppageTimer, attribute: .bottom, multiplier: 1, constant: 5).isActive = true

    minutes.widthAnchor.constraint(equalToConstant: 60).isActive = true
    seconds.widthAnchor.constraint(equalToConstant: 60).isActive = true


    layoutIfNeeded()
}

I cannot see any reason why all other constraints would work perfectly (even height does provided it refers to the height of another view, and is not just a constant value), but height and width are just ignored when defined as a constant. Debug log is totally silent, it does not object to any constraints.

I also notice that when debugging, the height constraint is set as it executes the height constraint line, but looking at the constraints after view.layoutIfNeeded() the height constraint is no more...

(lldb) po stopTimer.constraints
▿ 1 element
  - 0 : <NSLayoutConstraint:0x6000000997d0 NetScore.StoppageTimer:0x7fc3bff223d0.height == 100   (active)>

(lldb) po stopTimer.constraints
▿ 11 elements
  - 0 : <NSLayoutConstraint:0x60c00009d6f0 V:|-(0)-[UIView:0x7fc3bff225f0]   (active, names: '|':NetScore.StoppageTimer:0x7fc3bff223d0 )>
  - 1 : <NSLayoutConstraint:0x60c000281090 UIView:0x7fc3bff225f0.bottom == NetScore.StoppageTimer:0x7fc3bff223d0.bottom   (active)>
  - 2 : <NSLayoutConstraint:0x60c0002810e0 H:|-(0)-[UIView:0x7fc3bff225f0]   (active, names: '|':NetScore.StoppageTimer:0x7fc3bff223d0 )>
  - 3 : <NSLayoutConstraint:0x60c000281130 UIView:0x7fc3bff225f0.trailing == NetScore.StoppageTimer:0x7fc3bff223d0.trailing   (active)>
  - 4 : <NSLayoutConstraint:0x60c000281180 V:|-(10)-[UISegmentedControl:0x7fc3bff23f10]   (active, names: '|':NetScore.StoppageTimer:0x7fc3bff223d0 )>
  - 5 : <NSLayoutConstraint:0x60c0002811d0 H:|-(20)-[UISegmentedControl:0x7fc3bff23f10]   (active, names: '|':NetScore.StoppageTimer:0x7fc3bff223d0 )>
  - 6 : <NSLayoutConstraint:0x60c000281220 UISegmentedControl:0x7fc3bff23f10.trailing == NetScore.StoppageTimer:0x7fc3bff223d0.trailing - 20   (active)>
  - 7 : <NSLayoutConstraint:0x60c0002812c0 V:[UISegmentedControl:0x7fc3bff23f10]-(0)-[UIStackView:0x7fc3bff23d00]   (active)>
  - 8 : <NSLayoutConstraint:0x60c000281310 UIStackView:0x7fc3bff23d00.centerX == NetScore.StoppageTimer:0x7fc3bff223d0.centerX   (active)>
  - 9 : <NSLayoutConstraint:0x60c00009f360 UIButton:0x7fc3bff23080'Continue'.centerX == UIView:0x7fc3bff225f0.centerX   (active)>
  - 10 : <NSLayoutConstraint:0x60c0002813b0 V:[UIStackView:0x7fc3bff23d00]-(5)-[UIButton:0x7fc3bff23080'Continue']   (active)>
2
Could you provide a screenshot, log? as much output info as possibleVyacheslav
Do you want the content of stopTimer view to determine its height?DonMag

2 Answers

0
votes

The problem you're facing is actually not related to the height constraint as far as I can see.

You need to add this line:

stopTimer.translatesAutoresizingMaskIntoConstraints = false

The thing is, the default value of this property is true and it means that the view will have only the set of constraints that were generated automatically based on view's frame. And any of the constraints you are adding afterwards will not work. false means that you are not relying on the authorizing mask and you want to configure constraints by yourself.

Hope it helps!

0
votes

In setStoppageTimerConstraints(), you are saying:

NSLayoutConstraint(item: bgView, attribute: .top,      relatedBy: .equal, toItem: self, attribute: .top,      multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: bgView, attribute: .bottom,   relatedBy: .equal, toItem: self, attribute: .bottom,   multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: bgView, attribute: .leading,  relatedBy: .equal, toItem: self, attribute: .leading,  multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: bgView, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: 0).isActive = true

Pin bgView to all four sides (so it should completely fill the StoppageTimer view).

Then...

NSLayoutConstraint(item: StoppageType, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1, constant: 10).isActive = true
NSLayoutConstraint(item: StoppageType, attribute: .leading, relatedBy: .equal, toItem: self, attribute: .leading, multiplier: 1, constant: 20).isActive = true
NSLayoutConstraint(item: StoppageType, attribute: .trailing, relatedBy: .equal, toItem: self, attribute: .trailing, multiplier: 1, constant: -20).isActive = true

Pin StoppageType (a segmented control) leading and trailing edges, and pin its Top 10-pts from the Top of the view.

Then...

NSLayoutConstraint(item: StoppageTimer, attribute: .top,      relatedBy: .equal, toItem: StoppageType, attribute: .bottom,          multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: StoppageTimer, attribute: .centerX,   relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: StoppageTimer, attribute: .width,  relatedBy: .equal, toItem: nil, attribute: .notAnAttribute,      multiplier: 1, constant: 150).isActive = true

Pin StoppageTimer (a stack view) leading and trailing edges, and pin its Top 0-pts from the Bottom of StoppageType.

Then...

NSLayoutConstraint(item: butCont, attribute: .centerX, relatedBy: .equal, toItem: bgView, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: butCont, attribute: .top, relatedBy: .equal, toItem: StoppageTimer, attribute: .bottom, multiplier: 1, constant: 5).isActive = true

Pin butCont (a button) centerX, and pin its Top 5-pts from the Bottom of StoppageTimer.

So far, so good. But... You have forgotten to add a constraint to control the Height of the view itself.

So, add this line:

NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: butCont, attribute: .bottom, multiplier: 1.0, constant: 10.0).isActive = true

This says the view Bottom should be equal to the Bottom of butCont + 10-pts.

Now you can add stopTimer to your VC's view, and you only need to set its leading, trailing and top constraints. The constraints on the content of stopTimer will define its Height.


Edit: Clarification on why setting the Height constraint in the original code was not working...

At the end of setConstraints() in your VC, you're doing this:

// Stoppage Timer
NSLayoutConstraint(item: stopTimer, attribute: .top,      relatedBy: .equal, toItem: butSwitch, attribute: .bottom,         multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: stopTimer, attribute: .height,   relatedBy: .equal, toItem: nil,       attribute: .notAnAttribute, multiplier: 1, constant: 100).isActive = true
NSLayoutConstraint(item: stopTimer, attribute: .leading,  relatedBy: .equal, toItem: view,      attribute: .leadingMargin,  multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: stopTimer, attribute: .trailing, relatedBy: .equal, toItem: view,      attribute: .trailingMargin, multiplier: 1, constant: 0).isActive = true

which is setting the top, leading and trailing constraints and a Height constraint.

In your StoppageTimer view, you implemented traitCollectionDidChange() to add / update your constraints (it calls setStoppageTimerConstraints()). At the beginning of setStoppageTimerConstraints(), you remove all of its constraints. This would seem to be ok, except...

stopTimer view's top, leading and trailing constraints belong to your VC's view, whereas stopTimer view's Height constraint belongs to stopTimer.view.

traitCollectionDidChange() gets called more than once. In fact, it gets called after you have set the Height constraint. So:

constraints.forEach { constraint in constraint.isActive = false }

removes the Height constraint you just set from the VC.

Hope that makes sense.