Jack Wu
Jack Wu

Reputation: 37

Is there a simple way to delete specific custom cells from a UITableView?

I am trying to instantiate empty Buyer cells (custom cell) in my table view and then have the user populate the buyers' names. When the user presses the delete button for a row/cell, it should delete the corresponding row/cell regardless of whether or not the textfield for that row has been populated or not. Clearly, I am not getting the desired behavior. For example, when I press delete Row0 (whose textfield says "Buyer 0") and the tableview reloads, Buyer 0 is still there, but one of the empty Buyer cells at the end gets deleted instead.

import UIKit

class EntryAlertViewController: UIViewController {

    //Fields/Table
    @IBOutlet weak var itemField: UITextField!
    @IBOutlet weak var priceField: UITextField!
    @IBOutlet weak var tableView: UITableView!

    //Visual Components
    @IBOutlet weak var mainView: UIView!
    @IBOutlet weak var titleView: UIView!
    @IBOutlet weak var splitItemButton: UIButton!
    @IBOutlet weak var cancelButton: UIButton!
    @IBOutlet weak var addItemButton: UIButton!

    //Commonly Used Objects/Variables
    var potentialBuyers: [String] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        potentialBuyers.append("")
        tableView.dataSource = self
        tableView.register(UINib(nibName: "BuyerCell", bundle: nil), forCellReuseIdentifier: "ReusableCell")
    }

    override func viewWillAppear(_ animated: Bool) {
    }

    @IBAction func splitItemPressed(_ sender: UIButton) {
        potentialBuyers.append("")
        tableView.reloadData()
    }

}

Here are the tableview datasource and the delete button delegate.

extension EntryAlertViewController: UITableViewDataSource, DeleteButtonDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return potentialBuyers.count
    }

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

        let cell = tableView.dequeueReusableCell(withIdentifier: "ReusableCell", for: indexPath) as! BuyerCell
        cell.deleteButtonDelegate = self
        cell.indexPath = indexPath
        cell.nameField.text = cell.buyerName
        if potentialBuyers.count == 1 {
            cell.deleteButton.isHidden = true
        } else {
            cell.deleteButton.isHidden = false
        }
        return cell
    }


    func deletePressed(index: Int) {
        potentialBuyers.remove(at: index)
        tableView.reloadData()
    }


}

And here is my BuyerCell class with the UITextFieldDelegate as an extension.

import UIKit

protocol DeleteButtonDelegate {
    func deletePressed(index: Int)
}

class BuyerCell: UITableViewCell {

    @IBOutlet weak var deleteButton: UIButton!
    @IBOutlet weak var nameField: UITextField!

    var deleteButtonDelegate: DeleteButtonDelegate!
    var indexPath: IndexPath!
    var buyerName: String?

    override func awakeFromNib() {
        super.awakeFromNib()
        self.nameField.delegate = self
    }

    @IBAction func deletePressed(_ sender: UIButton) {
        //print the indexPath.row that this was pressed for
        print("delet pressed for \(indexPath.row)")
        self.deleteButtonDelegate?.deletePressed(index: indexPath.row)
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

}

extension BuyerCell: UITextFieldDelegate {

    func textFieldDidBeginEditing(_ textField: UITextField) {
        print("textFieldDidBeginEditing")
        buyerName = nameField.text
    }

    func textFieldDidEndEditing(_ textField: UITextField) {
        print("textFieldDidEndEditing")
        buyerName = nameField.text
    }

}

Upvotes: 0

Views: 644

Answers (2)

vadian
vadian

Reputation: 285082

There is a major issue in your code. You are not updating the data model so the changes in the cells are lost when the user scrolls.

Rather then quite objective-c-ish protocol/delegate in Swift callback closures are much more convenient and efficient. You can use one callback for both updating the model and deleting the cell.

Replace the BuyerCell cell with

class BuyerCell: UITableViewCell {

    @IBOutlet weak var deleteButton: UIButton!
    @IBOutlet weak var nameField: UITextField!

    var callback : ((UITableViewCell, String?) -> Void)?

    override func awakeFromNib() {
        super.awakeFromNib()
        self.nameField.delegate = self
    }

    @IBAction func deletePressed(_ sender: UIButton) {
        callback?(self, nil)
    }
}

extension BuyerCell: UITextFieldDelegate {

    func textFieldDidBeginEditing(_ textField: UITextField) {
        print("textFieldDidBeginEditing")
        callback?(self, nameField.text)
    }

    func textFieldDidEndEditing(_ textField: UITextField) {
        print("textFieldDidEndEditing")
        callback?(self, nameField.text)
    }
}

In the controller in cellForRow assign the callback and handle the actions. The actions work also reliably if cells are reordered, inserted or deleted.

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

    let cell = tableView.dequeueReusableCell(withIdentifier: "ReusableCell", for: indexPath) as! BuyerCell
    let buyerName = potentialBuyers[indexPath.row]
    cell.nameField.text = buyerName
    cell.callback = { [unowned self] cCell, cName in
        let currentIndexPath = tableView.indexPath(for: cCell)!
        if let name = cName {
             self.potentialBuyers[currentIndexPath.row] = name
        } else {
            self.potentialBuyers.remove(at: currentIndexPath.row)
            tableView.deleteRows(at: [currentIndexPath], with: .fade)
        }
    }
    cell.deleteButton.isHidden = potentialBuyers.count == 1
    return cell
}

Upvotes: 2

Paulw11
Paulw11

Reputation: 114855

Your problem is in this line

cell.nameField.text = cell.buyerName

Cells are reused from a reuse pool, so you can't rely on the cell holding any particular state or value.

Your buyer name needs to come from your data model array.

Something like

cell.nameField.text = self.potentialBuyers[indexPath.row]

Reloading the whole tableview is a bit excessive when you have only deleted a single row; Just delete the relevant row.

You can also clean up your delegation protocol so that there is no need for the cell to track its indexPath -

protocol DeleteButtonDelegate {
    func deletePressed(in cell: UITableViewCell)
}

In your cell:

@IBAction func deletePressed(_ sender: UIButton) {
    self.deleteButtonDelegate?.deletePressed(in: self)
}

In your view controller:

func deletePressed(in cell: UITableViewCell) {
    guard let indexPath = tableView.indexPath(for: cell) else {
        return
    }
    potentialBuyers.remove(at: indexPath.row)
    tableView.deleteRows(at:[indexPath], with: .automatic)
}

Upvotes: 2

Related Questions