Jessica Kimble
Jessica Kimble

Reputation: 535

UITableView cells are being reused/repeated

I know there are many questions similar to mine, however many of them are outdated, and others did not work with my case.

I have a questionnaire in a UITableView that contains 5 questions and 3 choices for each. choice is a UIButton that changes image when clicked. BUT when the first questions is answered, the fifth question gets answered as well!

Here is my tableView methods:

    var questions: [Question] = []

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return questions.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let question = questions[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: "choicesCell") as! ChoicesCell

        cell.setQuestion(question: question)
        cell.selectionStyle = .none
        return cell
    }

This is how I setup the images to choices buttons when they are selected:

    @IBAction func answerOneTapped(_ sender: UIButton) {
    delegate?.didTapAnswerOne()
    print("answerOneTapped")
    answerOneButton.setImage(#imageLiteral(resourceName: "ElipseSelected"), for: .normal)
    answerTwoButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
    answerThreeButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
}

@IBAction func answerTwoTapped(_ sender: UIButton) {
    delegate?.didTapAnswerTwo()
    print("answerTwoTapped")
    answerOneButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
    answerTwoButton.setImage(#imageLiteral(resourceName: "ElipseSelected"), for: .normal)
    answerThreeButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
}

@IBAction func answerThreeTapped(_ sender: UIButton) {
    delegate?.didTapAnswerThree()
    print("answerThreeTapped")
    answerOneButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
    answerTwoButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
    answerThreeButton.setImage(#imageLiteral(resourceName: "ElipseSelected"), for: .normal)
}

I tried several solutions with no luck like:

questions.removeAll()

in cellForRowAt and in ViewWillAppear

I also tried to setup the buttons to un-selected choice in:

    override func prepareForReuse() {
    super.prepareForReuse()
    answerOneButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
    answerTwoButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
    answerThreeButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
}

I've already spent many hours trying to solve this problem. and I read Apple docs regarding dequeueReusableCell and collected many info with no result...Please anyone help!

Upvotes: 0

Views: 126

Answers (3)

andrewbuilder
andrewbuilder

Reputation: 3791

From the Apple documentation prepareForReuse()...

For performance reasons, you should only reset attributes of the cell that are not related to content, for example, alpha, editing, and selection state. The table view's delegate in tableView(_:cellForRowAt:) should always reset all content when reusing a cell. If the cell object does not have an associated reuse identifier, this method is not called.

The most important point in this extract...

The table view's delegate in tableView(_:cellForRowAt:) should always reset all content when reusing a cell.

My suggestion is to reset the content in tableView(_:cellForRowAt:).

For example...

cell.answerOneButton.setImage(UIImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
cell.answerTwoButton.setImage(UIImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
cell.answerThreeButton.setImage(UIImage(#imageLiteral(resourceName: "Elipse"), for: .normal)

Forgive me if this sounds condescending (not intended) but it will help to think of the model and the view as separate. The view doesn’t know the (selection) state of the model, and the model doesn’t manage the view. The table view controller is the communicator (controller) between the two.

So maybe the fifth cell is not changing the answer to the fifth question in your model, instead the view is simply changing its presentation on screen based on code instructions in the controller. As your table view loads and/or reloads, it might be worth checking model values using breakpoints and/or print() to terminal.

Upvotes: 1

Sumerechie
Sumerechie

Reputation: 89

The problem is that even if prepareForReuse will work (strange that it didn't), that will reuse your first cell when you dont need it. The best solution that I always use is to save cells state in delegate and reset them in tableView(_:cellForRowAt:)

For example:

In your case you can add to Question class property choosedAnswer and enum with 3 cases

enum answer {
    case one
    case two
    case three
}

var choosedAnswer: answer?

In your cell.setQuestion(question:) function update button's images. Something like that:

    switch question.choosedAnswer {
    case .one:
        answerOneButton.setImage(#imageLiteral(resourceName: "ElipseSelected"), for: .normal)
        answerTwoButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
        answerThreeButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
    case .two:
        answerOneButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
        answerTwoButton.setImage(#imageLiteral(resourceName: "ElipseSelected"), for: .normal)
        answerThreeButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
    case .three:
        answerOneButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
        answerTwoButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
        answerThreeButton.setImage(#imageLiteral(resourceName: "ElipseSelected"), for: .normal)
    default:
        answerOneButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
        answerTwoButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
        answerThreeButton.setImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
    }

And dont forget to update models in your [Question] array after buttons select

func didTapAnswerOne(cell: UITableViewCell) {
    guard let indexPath = tableView.indexPath(for: sender) else {return}
    questions[indexPath.row] = questions[indexPath.row] == .one ? nil : .one
}

func didTapAnswerTwo(cell: UITableViewCell) {
    guard let indexPath = tableView.indexPath(for: sender) else {return}
    questions[indexPath.row] = questions[indexPath.row] == .two ? nil : .two
}

func didTapAnswerThree(cell: UITableViewCell) {
    guard let indexPath = tableView.indexPath(for: sender) else {return}
    questions[indexPath.row] = questions[indexPath.row] == .three ? nil : .three
}

In your cell delegate tap functions send self:

delegate?.didTapAnswerOne(cell: self)

Upvotes: 3

user256419
user256419

Reputation: 91

Your UIViewController that has tableView instance should know all the state that has been changed.

What I mean by that is:

Something like this. You can create object to achieve something similar

[Question 1] --> [State of Answer1, state of Answer2, state of Answer3]
[Question 2] --> [State of Answer1, state of Answer2, state of Answer3]
[Question 3] --> [State of Answer1, state of Answer2, state of Answer3]
so forth..

For instance:

Initial State:

Question 1 -> (False, False, False)

If user clicks on answer 2 of Question 1 then:

Question 1 -> (False, True, False)

then in cellForItem:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {


.
.
 (question[index.row].isTapped[answer])?
 cell.answerOneButtonsetImage(#imageLiteral(resourceName: "ElipseSelected"), for:   .normal) 
 :cell.answerOneButtonsetImage(#imageLiteral(resourceName: "Elipse"), for: .normal)
.
 
}

Upvotes: 0

Related Questions