[SOLVED] UITableView CustomCell Reuse (ImageView in CustomCell)

Issue

I’m pretty new to iOS dev and I have an issue with UITableViewCell.
I guess it is related to dequeuing reusable cell.
I added an UIImageView to my custom table view cell and also added a tap gesture to make like/unlike function (image changes from an empty heart(unlike) to a filled heart(like) as tapped and reverse). The problem is when I scroll down, some of the cells are automatically tapped. I found out why this is happening, but still don’t know how to fix it appropriately.
Below are my codes,

ViewController

import UIKit 
struct CellData {
var title: String
var done: Bool
}

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {     
var models = [CellData]()

private let tableView: UITableView = {
    let table = UITableView()
    table.register(TableViewCell.self, forCellReuseIdentifier: TableViewCell.identifier)
    return table
}()

override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(tableView)
    tableView.frame = view.bounds
    tableView.delegate = self
    tableView.dataSource = self
    configure()
}

private func configure() {
    self.models = Array(0...50).compactMap({
        CellData(title: "\($0)", done: false)
    })
}

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

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let model = models[indexPath.row]
    guard let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.identifier, for: indexPath) as? TableViewCell else {
        return UITableViewCell()
    }
    
    cell.textLabel?.text = model.title

    return cell
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    tableView.deselectRow(at: indexPath, animated: true)
    tableView.reloadData()

}
}

TableViewCell

import UIKit
class TableViewCell: UITableViewCell {
let mainVC = ViewController()
static let identifier = "TableViewCell"

let likeImage: UIImageView = {
    let imageView = UIImageView()
    imageView.image = UIImage(systemName: "heart")
    imageView.tintColor = .darkGray
    imageView.isUserInteractionEnabled = true
    imageView.translatesAutoresizingMaskIntoConstraints = false
    return imageView
}()


override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    contentView.addSubview(likeImage)
    layout()
    //Tap Gesture Recognizer 실행하기
    let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapImageView(_:)))
    likeImage.addGestureRecognizer(tapGestureRecognizer)
    
}

required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
    super.layoutSubviews()
}
override func prepareForReuse() {
    super.prepareForReuse()
}

private func layout() {
    likeImage.widthAnchor.constraint(equalToConstant: 30).isActive = true
    likeImage.heightAnchor.constraint(equalToConstant: 30).isActive = true
    likeImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
    likeImage.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20).isActive = true
}

@objc func didTapImageView(_ sender: UITapGestureRecognizer) {        

    if likeImage.image == UIImage(systemName: "heart.fill"){
        likeImage.image = UIImage(systemName: "heart")
        likeImage.tintColor = .darkGray
        

    } else {
        likeImage.image = UIImage(systemName: "heart.fill")
        likeImage.tintColor = .systemRed

    }
    
}
}

This gif shows how it works now.
enter image description here

I’ve tried to use "done" property in CellData structure to capture the status of the uiimageview but failed (didn’t know how to use that in the correct way).
I would be so happy if anyone can help this!

Solution

You’ve already figured out that the problem is cell reuse.

When you dequeue a cell to be shown, you are already setting the cell label’s text based on your data:

cell.textLabel?.text = model.title

you also need to tell the cell whether to show the empty or filled heart image.

And, when the user taps that image, your cell needs to tell the controller to update the .done property of your data model.

That can be done with either a protocol/delegate pattern or, more commonly (particularly with Swift), using a closure.

Here’s a quick modification of the code you posted… the comments should give you a good idea of what’s going on:

struct CellData {
    var title: String
    var done: Bool
}

class ShinViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
    var models = [CellData]()
    
    private let tableView: UITableView = {
        let table = UITableView()
        table.register(ShinTableViewCell.self, forCellReuseIdentifier: ShinTableViewCell.identifier)
        return table
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(tableView)
        tableView.frame = view.bounds
        tableView.delegate = self
        tableView.dataSource = self
        configure()
    }
    
    private func configure() {
        self.models = Array(0...50).compactMap({
            CellData(title: "\($0)", done: false)
        })
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return models.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: ShinTableViewCell.identifier, for: indexPath) as! ShinTableViewCell
        
        let model = models[indexPath.row]
        
        cell.myLabel.text = model.title
        
        // set the "heart" to true/false
        cell.isLiked = model.done
        
        // closure
        cell.callback = { [weak self] theCell, isLiked in
            guard let self = self,
                  let pth = self.tableView.indexPath(for: theCell)
            else { return }
            
            // update our data
            self.models[pth.row].done = isLiked
        }
        
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
    }

}

class ShinTableViewCell: UITableViewCell {
    
    // we'll use this closure to communicate with the controller
    var callback: ((UITableViewCell, Bool) -> ())?
    
    static let identifier = "TableViewCell"
    
    let likeImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.image = UIImage(systemName: "heart")
        imageView.tintColor = .darkGray
        imageView.isUserInteractionEnabled = true
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
    }()
    
    let myLabel: UILabel = {
        let v = UILabel()
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    // we'll load the heart images once in init
    //  instead of loading them every time they change
    var likeIMG: UIImage!
    var unlikeIMG: UIImage!
    
    var isLiked: Bool = false {
        didSet {
            // update the image in the image view
            likeImageView.image = isLiked ? likeIMG : unlikeIMG
            // update the tint
            likeImageView.tintColor = isLiked ? .systemRed : .darkGray
        }
    }
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        // make sure we load the heart images
        guard let img1 = UIImage(systemName: "heart"),
              let img2 = UIImage(systemName: "heart.fill")
        else {
            fatalError("Could not load the heart images!!!")
        }
        
        unlikeIMG = img1
        likeIMG = img2
        
        // add label and image view
        contentView.addSubview(myLabel)
        contentView.addSubview(likeImageView)

        layout()
        
        //Tap Gesture Recognizer 실행하기
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapImageView(_:)))
        likeImageView.addGestureRecognizer(tapGestureRecognizer)
        
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override func layoutSubviews() {
        super.layoutSubviews()
    }
    override func prepareForReuse() {
        super.prepareForReuse()
    }
    
    private func layout() {

        // let's use the "built-in" margins guide
        let g = contentView.layoutMarginsGuide
        
        // image view bottom constraint
        let bottomConstraint = likeImageView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
        // this will avoid auto-layout complaints
        bottomConstraint.priority = .required - 1
        
        NSLayoutConstraint.activate([
            
            // constrain label leading
            myLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            
            // center the label vertically
            myLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),
        
            // constrain image view trailing
            likeImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            
            // constrain image view to 30 x 30
            likeImageView.widthAnchor.constraint(equalToConstant: 30),
            likeImageView.heightAnchor.constraint(equalTo: likeImageView.widthAnchor),
            
            // constrain image view top
            likeImageView.topAnchor.constraint(equalTo: g.topAnchor),
            
            // activate image view bottom constraint
            bottomConstraint,

        ])

    }
    
    @objc func didTapImageView(_ sender: UITapGestureRecognizer) {
        
        // toggle isLiked (true/false)
        isLiked.toggle()
        
        // inform the controller, so it can update the data
        callback?(self, isLiked)
        
    }
    
}

Answered By – DonMag

Answer Checked By – David Marino (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published.