0

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.

JonGrimes20
  • 115
  • 9
  • What is the problem? Also note what you've depicted here is not using `IBAction`; you are linking the action using `addTarget` in your code. – shim Apr 18 '22 at 19:19
  • The problem is that the button image doesn't change when tapped. I know I'm not using an IBAction but I'll modify the question for more clarity – JonGrimes20 Apr 18 '22 at 19:26
  • Because cells are reused you will add you action handler multiple times. You also need to work out which cell was tapped, `tag` isn't a great solution since the default is 0 and it will break if rows are reordered. You also need to reload the relevant row when the button is tapped to update the cell appearance which compound your multiple action handler problem. – Paulw11 Apr 18 '22 at 19:26
  • See https://stackoverflow.com/questions/28659845/how-to-get-the-indexpath-row-when-an-element-is-activated/38941510#38941510 – Paulw11 Apr 18 '22 at 19:28
  • @Paulw11 I've tested the code and the mechanics of it work perfectly but for some reason the image of the button tapped doesn't change – JonGrimes20 Apr 18 '22 at 19:28
  • Also, having two separate but related arrays (products and quantities) is a code smell. You should use a `orderLineItem` object which has product and quantity properties. – Paulw11 Apr 18 '22 at 19:55

2 Answers2

0

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).

shim
  • 9,289
  • 12
  • 69
  • 108
  • How would I keep track of the selection state? Would it be within my ```Itemslected``` func or within the ```cellForRowAt``` func? – JonGrimes20 Apr 18 '22 at 19:36
  • There are various options; You could for example store an `Array` or a `Set` of selected indices in your class and add / remove items upon selection. Or if you went with the `UITableView` approach you could use [`indexPathsForSelectedRows`](https://developer.apple.com/documentation/uikit/uitableview/1614864-indexpathsforselectedrows). – shim Apr 18 '22 at 19:38
0

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.

DharmanBot
  • 1,066
  • 2
  • 6
  • 10
MBT
  • 1,381
  • 1
  • 6
  • 10