Home > Software engineering >  How to handle Button Actions for Buttons within a UITableViewCell?
How to handle Button Actions for Buttons within a UITableViewCell?

Time:04-19

I'm working on a shopping app project where customers can browse through products and add them to their cart, but am having trouble figuring out how to handle button presses within the tableViewCell. They way I want it to work is that when the empty circle button is pressed, the image changes to a filled circle with a checkmark, and the product within that cell is added to the customers "cart". The "cart" consists of two arrays products and quantities held within my CustomerOrder object.

Here's what the tableView looks like:

image of my UI

Here's my code so far:

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

    let cell = tableView.dequeueReusableCell(withIdentifier: "ProductToBuyCell") as! ProductToBuyCell
    
    //Configure the Selection Button
    cell.selectButton.tag = indexPath.row
    cell.selectButton.addTarget(self, action: #selector(ItemSelected), for: .touchUpInside)
    let product = productsArray[indexPath.row]
    //configure cell
    cell.productImg.image = product.productPhoto
    cell.productImg.contentMode = .scaleAspectFit
    cell.productName.text = product.Name
    cell.price.text = "$\(product.price)"
    
    return cell
}
// func for when the selectButton is tapped
// tag is equal to indexPath.row
@objc func ItemSelected(sender: UIButton) {
    sender.imageView?.image = UIImage(systemName: "checkmark.circle.fill")
    let product = productsArray[sender.tag]
    newOrder.products.append(product)
    newOrder.quantities.append(1)
}

So if someone could please explain how to properly handle events that are caused by elements within a UITableViewCell and how to get the buttons image to change, that would be very much appreciated.

CodePudding user response:

UIButton's imageView is read-only — to update its image you should use setImage. Note you'll need to keep track of the selection state of the rows or else the selection state will be reset whenever the cell reloads (e.g. it scrolls out and back into view). Also presumably you'll want to handle the case where the button is deselected.

Also, since you're only handling the selection on the UIButton in the cell, tapping the cell will not have any effect on the button's state. You could instead implement tableView(_:didSelectRowAt:) and handle the selection there, and just have a simple UIImageView instead of a UIButton in your cell. You could also consider using the accessoryView.

UITableView also has built-in support for multiple selection, so if you turn that on it can keep track of the selected cells for you, and you just need to update the appearance of the cells accordingly.

As for using the tag to figure out the cell index, it's true it isn't very robust but that wasn't the question. Definitely worth checking out the linked question in the comments above for some good discussion around that.

And yes, if you're going to stick with this approach you should at least remove any existing actions before adding a new one to avoid duplicate events, e.g. button.removeTarget(nil, action: nil, for: .allEvents).

CodePudding user response:

I assume your Product model is like this.

struct Product {
    let name: String
    let photo: UIImage!
    let price: Double
}

To keep track of the selected product. You need to preserve the state of the cell. Create another model like this

struct ProductSelection {
    let product: Product!
    var isSelected: Bool = false
}

As @ Paulw11 mentioned that "tag isn't a great solution since the default is 0 and it will break if rows are reordered", you can use delegate pattern to get the selected product index.

Create a protocol of the ProductToBuyCell.

protocol ProductToBuyCellDelegate: AnyObject {
    func selectedCell(sender: ProductToBuyCell)
}

Add a delegate property to cell.

weak var delegate: ProductToBuyCellDelegate?

Also add an IBOutlet and IBAction of the button in ProductToBuyCell.

@IBOutlet weak var checkButton: UIButton!


@IBAction func buttonPressed(_ sender: UIButton) {
    delegate?.selectedCell(sender: self)
}

Then add a selectionProduct property in the cell and set text of UILabel and image of UIImage using property observer didSet

var productSelection: ProductSelection? {
    didSet {
        guard let productSelection = productSelection else { return }

        self.productNameLabel.text = productSelection.product.name
        self.priceLabel.text = "$\(productSelection.product.price)"
        self.productImg.image = productSelection.product.photo
        
        if productSelection.isSelected {
            checkButton.setImage(UIImage(systemName: "checkmark.circle.fill"), for: .normal)
        } else {
            checkButton.setImage(UIImage(systemName: "circle"), for: .normal)
        }
    }
}

Modify the appearence of UIImage in awakeFromNib() method

override func awakeFromNib() {
    super.awakeFromNib()
    
    self.productImg.contentMode = .scaleAspectFit
}

Then modify the cellForRowAt method like below. Here, productSelectionArray is an array of ProductSection not Product.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "ProductToBuyCell") as! ProductToBuyCell
    cell.productSelection = productSelectionArray[indexPath.row]
    cell.delegate = self
    return cell
}

Now confirm the delegate of ProductToBuyCell to ViewController. Here, at first I change the state of the selected cell and then retrieve all the selected products using filter and map function in productSelectionArray. And then reload the row to update UI.

extension ViewController: ProductToBuyCellDelegate {
    func selectedCell(sender: ProductToBuyCell) {
        guard let tappedIndexPath = tableView.indexPath(for: sender) else { return }

        productSelectionArray[tappedIndexPath.row].isSelected.toggle()

        let selectedProducts = productSelectionArray.filter{ $0.isSelected }.map { (productSelection: ProductSelection) -> Product in
            return productSelection.product
        }

        tableView.reloadRows(at: [tappedIndexPath], with: .none)
    }
}

According to your UI it seems that all the selected products quantity is one, so you don't have to maintain another array for quantity.

Hope it helps.

  • Related