从自定义 Cell 中正确委托按钮操作以删除 UITableView 中的行

Properly delegate button action from custom Cell to delete rows in UITableView

仍然是一个 Swift 菜鸟,我一直在寻找适当的 way/best 实践来管理 UITableView 中的行删除(使用自定义 UserCell s) 基于使用委托在 UserCell 中点击 UIButton,这似乎是最干净的方法。

我遵循了这个例子:

我有什么

UserCell class

protocol UserCellDelegate {

    func didPressButton(_ tag: Int)
}

class UserCell: UITableViewCell {

    var delegate: UserCellDelegate?
    let addButton: UIButton = {

        let button = UIButton(type: .system)

        button.setTitle("Add +", for: .normal)
        button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)

        addSubview(addButton)
        addButton.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -6).isActive = true
        addButton.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
        addButton.heightAnchor.constraint(equalToConstant: self.frame.height / 2).isActive = true
        addButton.widthAnchor.constraint(equalToConstant: self.frame.width / 6).isActive = true
    }

    func buttonPressed(_ sender: UIButton) {

        delegate?.didPressButton(sender.tag)
    }
}

TableViewController class:

class AddFriendsScreenController: UITableViewController, UserCellDelegate {

    let cellId = "cellId"
    var users = [User]()

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

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

        let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! UserCell

        cell.delegate = self
        cell.tag = indexPath.row

        return cell
    }

    func didPressButton(_ tag: Int) {

        let indexPath = IndexPath(row: tag, section: 0)

        users.remove(at: tag)
        tableView.deleteRows(at: [indexPath], with: .fade)
    }
}

其中 users 中的 User 附加了对视图控制器中数据库的调用。

我的问题

我想要的

能够单击 table 每一行中的按钮,这会将其从 table 视图中删除。

我一定是犯了一些相当基本的错误,如果 Swift 骑士能启发我,我将不胜感激。

非常感谢。

试试这个 -

更新 didPressButton 方法如下 -

 func didPressButton(_ tag: Int) {
    let indexPath = IndexPath(row: tag, section: 0)
    users.remove(at: tag)
    tableView.reloadData()
}

您的代码中至少存在 3 个问题:

  • UserCell 你应该调用:
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)

一旦你的单元格被实例化(比如,从你的 init(style:reuseIdentifier:) 实现),那么 self 指的是 UserCell 的实际实例。

  • AddFriendsScreenControllertableView(_:cellForRowAt:) 中,您正在设置单元格本身的标签 (cell.tag = indexPath.row),但在 UserCellbuttonPressed(_:) 中,您正在使用按钮的标签。您应该将该函数修改为:
func buttonPressed(_ sender: UIButton) {

    //delegate?.didPressButton(sender.tag)
    delegate?.didPressButton(self.tag)
}
  • 如您所料,并根据 您应该在删除行后重新加载 table 视图,因为您的单元格标签将与其引用不同步 indexPaths .理想情况下,您应该避免依赖索引路径来识别单元格,但这是另一个主题。

编辑:
避免标签与索引路径不同步的一个简单解决方案是将每个单元格与它们应该表示的 User 对象相关联:

  • 首先在你的UserCellclass中添加一个user属性:
class UserCell: UITableViewCell {

    var user = User()   // default with a dummy user

    /* (...) */
}
  • 将此 属性 设置为 tableView(_:cellForRowAt:) 中正确的 User 对象:
//cell.tag = indexPath.row
cell.user = self.users[indexPath.row]
  • 修改您的 UserCellDelegate 协议方法的签名以传递针对单元存储的 user 属性 而不是其 tag:
protocol UserCellDelegate {

    //func didPressButton(_ tag: Int)
    func didPressButtonFor(_ user: User)

}
  • 相应地修改UserCellbuttonPressed(_:)行动:
func buttonPressed(_ sender: UIButton) {

    //delegate?.didPressButton(sender.tag)
    //delegate?.didPressButton(self.tag)
    delegate?.didPressButtonFor(self.user)
}
  • 最后,在您的 AddFriendsScreenController 中,根据数据源中的 User 位置确定要删除的正确行:
//func didPressButton(_ tag: Int) { /* (...) */ }   // Scrap this.

func didPressButtonFor(_ user: User) {

    if let index = users.index(where: { [=16=] === user }) {

        let indexPath = IndexPath(row: index, section: 0)

        users.remove(at: index)
        tableView.deleteRows(at: [indexPath], with: .fade)
    }
}

注意 if let index = ... 构造 (optional binding) and the triple === (identity operator)。

这种方法的缺点是它会在您的 UserUserCell class 之间创建紧密耦合。例如,最佳做法是使用更复杂的 MVVM pattern,但这确实是另一个主题...

网络上有很多 bad/old 代码,甚至在 SO 上也是如此。您发布的内容已写满 "bad practice"。所以首先要注意几点:

  • 不惜一切代价避免UITableViewController。有一个带有 table 视图的普通视图控制器
  • 除非您 100% 确定自己在做什么,否则代表应始终 weak
  • 命名协议和协议方法时更具体
  • 尽可能保留所有内容 private,否则使用 fileprivate。仅当您 100% 确定它是您要公开的值时才使用其余部分。
  • 不惜一切代价避免使用标签

以下是一个负责任的 table 视图示例,其中包含一个单元格类型,它有一个按钮,按下时会删除当前单元格。创建新项目时,可以将整个代码粘贴到初始 ViewController 文件中。在故事板中,table 视图向视图控制器添加了左、右、上、下约束和出口。还在 table 视图中添加了一个单元格,其中有一个按钮,该按钮具有单元格 MyTableViewCell 的出口,其标识符设置为 "MyTableViewCell".

剩下的在评论里说明。

class ViewController: UIViewController {

    @IBOutlet private weak var tableView: UITableView? // By default use private and optional. Always. For all outlets. Only expose it if you really need it outside

    fileprivate var myItems: [String]? // Use any objects you need.


    override func viewDidLoad() {
        super.viewDidLoad()

        // Attach table viw to self
        tableView?.delegate = self
        tableView?.dataSource = self

        // First refresh and reload the data
        refreshFromData() // This is to ensure no defaults are visible in the beginning
        reloadData()
    }

    private func reloadData() {

        myItems = nil

        // Simulate a data fetch
        let queue = DispatchQueue(label: "test") // Just for the async example

        queue.async {
            let items: [String] = (1...100).flatMap { "Item: \([=10=])" } // Just generate some string
            Thread.sleep(forTimeInterval: 3.0) // Wait 3 seconds
            DispatchQueue.main.async { // Go back to main thread
                self.myItems = items // Assign data source to self
                self.refreshFromData() // Now refresh the table view
            }
        }
    }

    private func refreshFromData() {
        tableView?.reloadData()
        tableView?.isHidden = myItems == nil
        // Add other stuff that need updating here if needed
    }

    /// Will remove an item from the data source and update the array
    ///
    /// - Parameter item: The item to remove
    fileprivate func removeItem(item: String) {

        if let index = myItems?.index(of: item) { // Get the index of the object

            tableView?.beginUpdates() // Begin updates so the table view saves the current state
            myItems = myItems?.filter { [=10=] != item } // Update our data source first
            tableView?.deleteRows(at: [IndexPath(row: index, section: 0)], with: .fade) // Do the table view cell modifications
            tableView?.endUpdates() // Commit the modifications
        }

    }

}

// MARK: - UITableViewDelegate, UITableViewDataSource

extension ViewController: UITableViewDelegate, UITableViewDataSource {

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

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCell", for: indexPath) as? MyTableViewCell {
            cell.item = myItems?[indexPath.row]
            cell.delegate = self
            return cell
        } else {
            return UITableViewCell()
        }
    }

}

// MARK: - MyTableViewCellDelegate

extension ViewController: MyTableViewCellDelegate {

    func myTableViewCell(pressedMainButton sender: MyTableViewCell) {

        guard let item = sender.item else {
            return
        }

        // Delete the item if main button is pressed
        removeItem(item: item)

    }

}



protocol MyTableViewCellDelegate: class { // We need ": class" so the delegate can be marked as weak

    /// Called on main button pressed
    ///
    /// - Parameter sender: The sender cell
    func myTableViewCell(pressedMainButton sender: MyTableViewCell)

}

class MyTableViewCell: UITableViewCell {

    @IBOutlet private weak var button: UIButton?

    weak var delegate: MyTableViewCellDelegate? // Must be weak or we can have a retain cycle and create a memory leak

    var item: String? {
        didSet {
            button?.setTitle(item, for: .normal)
        }
    }

    @IBAction private func buttonPressed(_ sender: Any) {

        delegate?.myTableViewCell(pressedMainButton: self)

    }
}

在您的情况下,String 应替换为 User。接下来,您将进行一些更改,例如单元格中的 didSet(例如 button?.setTitle(item.name, for: .normal)),过滤方法应使用 === 或比较一些 id 或其他内容.