NSLayoutConstraints is not respected in UITableViewCell

895 Views Asked by At

I have a UITableViewCell with an UILabel and an UIImageView. The image can be visible or hidden.

Here is my storyboard: storyboard screenshot

There's two constraints on the trailing part of the UILabel, one (a) that is equal to 8 with the UIImageView, another one (b) that is greater or equal to 8 with the right margin of the cell. I keep a reference of the first one (a), and I activate or deactivate the constraint if there is or not some sound.

Here is my code:

class MyTableViewCell: UITableViewCell {

    @IBOutlet weak var label: UILabel?
    @IBOutlet weak var icon: UIImageView?
    @IBOutlet weak var spaceBetweenIconAndLabelConstraint: NSLayoutConstraint?

    override func awakeFromNib() {
        super.awakeFromNib()
        icon?.image = UIImage(named: "sound")
    }

    func config(with name: String, hasSound: Bool) {
        label?.text = name
        configSound(hasSound)
    }

    private func configSound(_ hasSound: Bool) {
        icon?.isHidden = !hasSound
        spaceBetweenIconAndLabelConstraint?.isActive = hasSound
    }
}

I have a few cells with the sound icon visible, a lot without. Here is what it looks like when a specific cell first appears:

good behaviour

And what it looks like when it comes back on the screen a second time:

bad behaviour

I do understand the problem is coming from the cell being reused. But I don't understand how I can prevent this behaviour. I tried to do:

override func prepareForReuse() {
    configSound(true)
}

to reactivate the constraint before reusing the cell, but it doesn't work.

2

There are 2 best solutions below

1
André Slotta On BEST ANSWER

I think the problem is the fact that you use a weak reference for your constraint. In that case the constraint gets removed as soon as its isActive property is set to false for the first time. From that on it is nil and can't be reactivated.

Solution: Use a strong reference by removing the weak keyword.

@IBOutlet var spaceBetweenIconAndLabelConstraint: NSLayoutConstraint!
0
Andrzej Michnia On

There are more than two ways to do it. If you are targeting iOS 9+, I would strongly recommend to use stack views. They do exactly what you need, without the need to manually add/remove/activate/deactivate constraints.

The UI will look like that:

stack view setup

Horizontal stack view (8 to leading, 8 to trailing, spacing equal 8) inside: 1. on left label 2. on right icon image view (optionally wrapped in iconContainer view, or just set aspectFit)

Update code:

class MyTableViewCellWithStackView: UITableViewCell {

    @IBOutlet weak var label: UILabel?
    @IBOutlet weak var iconContainer: UIView?

    func config(with name: String, hasSound: Bool) {
        label?.text = name
        iconContainer?.isHidden = !hasSound
    }
}

Whenever you hide icon/iconContainer, stack view will update itself and fill space accordingly.

If you can't use stack views (preferred), you can try this:

class MyTableViewCell: UITableViewCell {

    @IBOutlet weak var label: UILabel?
    @IBOutlet weak var icon: UIImageView?
    @IBOutlet weak var spaceBetweenIconAndLabelConstraint: NSLayoutConstraint?

    override func awakeFromNib() {
        super.awakeFromNib()
        icon?.image = UIImage(named: "sound")
    }

    func config(with name: String, hasSound: Bool) {
        label?.text = name
        configSound(hasSound)
    }

    private func configSound(_ hasSound: Bool) {
        icon?.isHidden = !hasSound
        guard hasSound else {
            spaceBetweenIconAndLabelConstraint?.isActive = false
            return
        }
        guard let icon = icon, let label = label else { return }

        let constraint = label.rightAnchor
                                .constraint(equalTo: icon.leftAnchor, constant: 8)
        constraint.isActive = true
        spaceBetweenIconAndLabelConstraint = constraint
    }
}