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