4

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.

magohamote
  • 1,484
  • 1
  • 17
  • 29
  • Do the cells change from having an image to not having an image on the fly (after they are rendered) or is it that once the cells are set by the table, they remain that way until the entire table is reloaded? – trndjc Dec 09 '18 at 18:53
  • @nard They do not change on the fly. If a cell has an image, it will always have it, or they will never have one. – magohamote Dec 09 '18 at 20:15
  • Then just create two types of cells, with an image and without, and let the data source determine which one is dequeued. Conditionally implementing constraints in reusable cells can produce unsavory results when the tables get big and the scrolling is fast—keep the cells as basic as possible for optimum table performance. – trndjc Dec 09 '18 at 20:22
  • The thing is that I simplified my code for the post but it does more stuff and I don't like code duplication and I don't know how to inherit from classes that has `IBOutlet` (or if this is even possible). – magohamote Dec 09 '18 at 22:12

2 Answers2

8

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!
André Slotta
  • 13,774
  • 2
  • 22
  • 34
  • 1
    Thank you very much, in top of solving the problem I learned that setting a weak constraint to false makes it nil! :) – magohamote Dec 09 '18 at 20:16
4

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
    }
}
Andrzej Michnia
  • 538
  • 3
  • 9