正确委派自定义单元格中的按钮操作以删除UITableView

时间:2017-02-14 12:32:25

标签: ios swift uitableview swift3 swift-protocols

仍然是一个Swift noob,我一直在寻找一种正确的方法/最佳实践来管理UITableView(使用自定义UserCell s)中的行删除,这是基于点击{{{ 1}} UIButton内部使用委托,这似乎是最干净的方式。

我按照这个例子:UITableViewCell Buttons with action

我有什么

UserCell类

UserCell

TableViewController类:

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

其中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) } } 中的User附加了对视图控制器中数据库的调用。

我的问题

  • 表视图每行中的按钮是可点击的,但不执行任何操作
  • 按钮似乎只有在长按"时才可点击,即手指在其上停留约0.5秒
  • 此方法是否可以保证users更新并且不会超出范围?即如果在索引0处删除了一行,则会删除" new"索引0处的行正常工作还是会删除索引1处的行?

我想要什么

能够单击表格每行中的按钮,这会将其从桌面视图中删除。

我必须得到一些相当基本的错误,如果一个斯威夫特骑士可以启发我,我会非常感激。

非常感谢提前。

3 个答案:

答案 0 :(得分:2)

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

  • UserCell中,您应致电:
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)

一旦您的单元格被实例化(例如,从init(style:reuseIdentifier:)的实现中),以便self引用UserCell的实际实例。

  • AddFriendsScreenController的{​​{1}}中,您正在设置单元格本身的标记(tableView(_:cellForRowAt:)),但在您使用的cell.tag = indexPath.row UserCell buttonPressed(_:)中按钮的标签。您应该将该功能修改为:
func buttonPressed(_ sender: UIButton) {

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

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

  • 首先向user班级添加UserCell媒体资源:
class UserCell: UITableViewCell {

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

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

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

}
  • 相应地修改UserCell的{​​{1}}行动:
buttonPressed(_:)
  • 最后,在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: { $0 === user }) { let indexPath = IndexPath(row: index, section: 0) users.remove(at: index) tableView.deleteRows(at: [indexPath], with: .fade) } } 构造(optional binding)和三重if let index = ...identity operator)。

这种方法的缺点是它会在===User类之间产生紧密耦合。最佳实践将指示使用更复杂的MVVM pattern,但这确实是另一个主题......

答案 1 :(得分:2)

网上有很多坏/旧代码,即使在SO上也是如此。你发布的内容有“不良做法”。所以先说几点:

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

以下是具有单个单元格类型的负责表格视图的示例,其中有一个按钮可在按下时删除当前单元格。创建新项目时,可以将整个代码粘贴到初始ViewController文件中。在故事板中,表格视图添加了约束左,右,顶部,底部和视图控制器的出口。此外,还在表格视图中添加了一个单元格,其中包含一个指向单元格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: \($0)" } // 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 { $0 != 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或其他内容

答案 2 :(得分:0)

试试这个 -

更新didPressButton方法,如下所示 -

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