I'm having trouble with programmatically setting layout constraints in a custom UITableViewCell

35 Views Asked by At

I'm working on an iOS app which is a basic Pokedex, which will hopefully teach me the basics of working with UIKit (not using Storyboards for this project). When I run my app everything lays out fine. However when I scroll down there are some constraints that get messed up causing buttons to appear off screen. I can't figure out what would be causing this to happen. I think that some constraints are being applied or are not being applied when a cell is reused but I can't find an obvious error in my constraint code.

func set(pokemon: Pokemon) {
    name.text = pokemon.name
    var spriteName: String?

    if let pokemonForm = pokemon.form {
        addSubview(form)
        form.text = pokemonForm
        setNameLabelConstraints(hasForm: true)
        setFormLabelConstraints()
        switch pokemonForm {
            case "Mega":
                spriteName = String(pokemon.speciesID) + "-mega-sprite"
            case "Mega X":
                spriteName = String(pokemon.speciesID) + "-mega-x-sprite"
            case "Mega Y":
                spriteName = String(pokemon.speciesID) + "-mega-y-sprite"
            default:
                spriteName = String(pokemon.speciesID) + "-sprite"
        }
    } else {
        spriteName = String(pokemon.id) + "-sprite"
        form.removeFromSuperview()
        setNameLabelConstraints(hasForm: false)
    }
    sprite.image = UIImage(named: spriteName!)
    type1.setTitle(pokemon.type1, for: .normal)
    styleTypeFilterButton(button: type1)
    type1.addTarget(self, action: #selector(buttonTapped(sender:)), for: UIControl.Event.touchUpInside)
    if let secondType = pokemon.type2 {
        addSubview(type2)
        type2.setTitle(secondType, for: .normal)
        styleTypeFilterButton(button: type2)
        setType2ButtonConstraints()
        setType1ButtonConstraints(twoButtons: true)
        type2.addTarget(self, action: #selector(buttonTapped(sender:)), for: UIControl.Event.touchUpInside)
    } else {
        type2.removeFromSuperview()
        setType1ButtonConstraints(twoButtons: false)
    }

}

func setNameLabelConstraints(hasForm: Bool) {
    name.translatesAutoresizingMaskIntoConstraints = false
    name.leadingAnchor.constraint(equalTo: sprite.trailingAnchor, constant: 20).isActive = true
    if hasForm == false {
        name.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
    } else {
        name.heightAnchor.constraint(equalToConstant: 40).isActive = true
        name.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = false
    }
}

func setType1ButtonConstraints(twoButtons: Bool) {
    //type1.translatesAutoresizingMaskIntoConstraints = false
    if twoButtons == true {
        type1.trailingAnchor.constraint(equalTo: type2.leadingAnchor, constant: -15).isActive = true
    } else {
        type1.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -25).isActive = true
    }
}

func setType2ButtonConstraints() {
    //type2.translatesAutoresizingMaskIntoConstraints = false
    type2.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -25).isActive = true
}

func setFormLabelConstraints() {
    form.translatesAutoresizingMaskIntoConstraints = false
    form.leadingAnchor.constraint(equalTo: sprite.trailingAnchor, constant: 20).isActive = true
    form.heightAnchor.constraint(equalToConstant: 40).isActive = true
    form.topAnchor.constraint(equalTo: name.bottomAnchor, constant: -20).isActive = true
    form.font = form.font.withSize(14)
}

func styleTypeFilterButton(button: UIButton) {
    button.translatesAutoresizingMaskIntoConstraints = false
    button.heightAnchor.constraint(equalToConstant: 25).isActive = true
    button.widthAnchor.constraint(equalToConstant: 65).isActive = true
    button.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true

    button.layer.cornerRadius = 12
    button.setTitleColor(UIColor.white, for: .normal)
    button.titleLabel?.font = button.titleLabel?.font.withSize(14)

    switch button.titleLabel?.text {
        case "Normal":
            button.backgroundColor = UIColor(hexValue: "a8a878")
        case "Fighting":
            button.backgroundColor = UIColor(hexValue: "c03028")
        case "Flying":
            button.backgroundColor = UIColor(hexValue: "a890f0")
        case "Poison":
            button.backgroundColor = UIColor(hexValue: "a040a0")
        case "Ground":
            button.backgroundColor = UIColor(hexValue: "e0c068")
        case "Rock":
            button.backgroundColor = UIColor(hexValue: "b8a038")
        case "Bug":
            button.backgroundColor = UIColor(hexValue: "a8b820")
        case "Ghost":
            button.backgroundColor = UIColor(hexValue: "705898")
        case "Steel":
            button.backgroundColor = UIColor(hexValue: "b8b8d0")
        case "Fire":
            button.backgroundColor = UIColor(hexValue: "f08030")
        case "Water":
            button.backgroundColor = UIColor(hexValue: "6890f0")
        case "Grass":
            button.backgroundColor = UIColor(hexValue: "78C850")
        case "Electric":
            button.backgroundColor = UIColor(hexValue: "f8d030")
        case "Psychic":
            button.backgroundColor = UIColor(hexValue: "f85888")
        case "Ice":
            button.backgroundColor = UIColor(hexValue: "98d8d8")
        case "Dragon":
            button.backgroundColor = UIColor(hexValue: "7038f8")
        case "Dark":
            button.backgroundColor = UIColor(hexValue: "705848")
        case "Fairy":
            button.backgroundColor = UIColor(hexValue: "ee99ac")
        case "???":
            button.backgroundColor = UIColor(hexValue: "68a090")
        case "Shadow":
            button.backgroundColor = UIColor(hexValue: "705848")
        default:
            button.backgroundColor = UIColor(hexValue: "a8a878")
    }
}

Initial view on page load

View after scrolling

If someone could explain why it lays out correctly when the app first runs, but gets messed up after the cells get reused I would greatly appreciate it! I've never worked with autolayout before, so I might also have in error in my autolayout code but I'm not sure if anything is wrong. Thank you in advance!!

1

There are 1 best solutions below

1
Jamie A On BEST ANSWER

I'm making some guesses about how your code works, but in short the problem is that you're not removing old layout constraints when you recycle your cells.

Let's say a cell gets created and for the first entry it has to display, you call setType1ButtonConstraints(twoButtons: false). type1 is now pinned to the trailing edge of the cell. Then that cell is recycled and for the new entry it has to display you call setType2ButtonConstraints() and then setType1ButtonConstraints(twoButtons: true). Your code will pin type2 to the trailing edge of the cell, and then pin type1 to be before type2, but you never got rid of the constraint that was made from the first entry. You now have a set of unsatisfiable constraints (there will be a warning about this in the console) and the system has to break one.

To solve this you should keep references to these constraints and remove them in prepareForReuse(). You could also use a horizontal UIStackView.