正确使用 UITableViewDataSource 和 Custom UITableViewCell Delegate 的方法

Correct way to use UITableViewDataSource and Custom UITableViewCell Delegate

我正在努力将 UITableViewDataSource 移到 UITableViewController 之外。但是我有一些自定义单元格有自己的委托,然后调用 tableView 重新加载。

我不确定处理此问题的正确方法是什么。这是我拥有的:

final class MyTableViewController: UITableViewController {
    
    lazy var myTableViewDataSource: MyTableViewDataSource = { MyTableViewDataSource(myProperty: MyProperty) }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = myTableViewDataSource
    }

}

单元格

 protocol MyTableViewCellDelegate: AnyObject {
    func doSomething(_ cell: MyTableViewCellDelegate, indexPath: IndexPath, text: String)
}


 final class MyTableViewCell: UITableViewCell, UITextFieldDelegate {
    
    @IBOutlet weak var packageSizeTextField: UITextField!
    
    weak var delegate: MyTableViewCellDelegate?
    
    var indexPath = IndexPath()
    
    override func awakeFromNib() {
        super.awakeFromNib()
    }
    
    func configureCell() {
        // configureCell...
    }
    
    func textFieldDidChangeSelection(_ textField: UITextField) {
        print(#function)
        delegate?.doSomething(self, indexPath: indexPath, text: textField.text ?? "")
    }
}

数据源

final class MyTableViewDataSource: NSObject, UITableViewDataSource {
    
    var myProperty: MyProperty!
    
    init(myProperty: MyProperty) {
        self.myProperty = myProperty
    }
    
    // ...
    
    func doSomething(_ cell: MyTableViewCell, indexPath: IndexPath, text: String) {
        // ...
        tableView.performBatchUpdates({
            tableView.reloadRows(at: [indexPath], with: .automatic)
        }) 
// ERROR - tableView doesn't exist
    }
    
}

我的问题是,如何获得此 class 为其提供源的 tableView 的访问权?像这样添加对tableView的引用就这么简单吗?

var tableView: UITableView
var myProperty: MyProperty!

init(myProperty: MyProperty, tableView: UITableView) {
            self.myProperty = myProperty
            self.tableView = tableView
        }

一个选择是让您的 MyTableViewController 符合您的 MyTableViewCellDelegate,然后在数据源 class.[=36 中将控制器设置为 cellForRowAt 中的委托=]

但是,使用闭包可能会好得多。

删除单元格中的 delegateindexPath 属性,并添加闭包 属性:

final class MyTableViewCell: UITableViewCell, UITextFieldDelegate {
    
    @IBOutlet weak var packageSizeTextField: UITextField!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        configureCell()
    }
    func configureCell() {
        // configureCell...
        packageSizeTextField.delegate = self
    }
    
    var changeClosure: ((String, UITableViewCell)->())?
    
    func textFieldDidChangeSelection(_ textField: UITextField) {
        print(#function)
        changeClosure?(textField.text ?? "", self)
//      delegate?.doSomething(self, indexPath: indexPath, text: textField.text ?? "")
    }
}

现在,在您的数据源 class 中,设置闭包:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let c = tableView.dequeueReusableCell(withIdentifier: "mtvc", for: indexPath) as! MyTableViewCell
    c.packageSizeTextField.text = myData[indexPath.row]
    c.changeClosure = { [weak self, weak tableView] str, c in
        guard let self = self,
              let tableView = tableView,
              let pth = tableView.indexPath(for: c)
        else {
            return
        }
        // update our data
        self.myData[pth.row] = str
        // do something with the tableView
        //tableView.reloadData()
    }
    return c
}

请注意,当您编写代码时,在文本字段中的第一次点击似乎不会执行任何操作,因为 textFieldDidChangeSelection 将立即被调用。


编辑

这是一个完整的示例,可以 运行 没有任何 Storyboard 连接。

该单元格创建一个标签和一个文本字段,将它们排列在垂直堆栈视图中。

第 0 行将隐藏文本字段,其标签文本将设置为来自 myData.

的串联字符串

其余行将隐藏标签。

闭包将在 .editingChanged(而不是 textFieldDidChangeSelection)上调用,因此不会在编辑开始时调用。

出于演示目的,还实现了行删除。

当任何行的文本字段中的文本发生变化时,第一行将被重新加载,当一行被删除时。

单元格Class

final class MyTableViewCell: UITableViewCell, UITextFieldDelegate {
    
    var testLabel = UILabel()
    var packageSizeTextField = UITextField()

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        configureCell()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        configureCell()
    }
    func configureCell() {
        // configureCell...
        let stack = UIStackView()
        stack.axis = .vertical
        stack.translatesAutoresizingMaskIntoConstraints = false
        
        testLabel.numberOfLines = 0
        testLabel.backgroundColor = .yellow
        
        packageSizeTextField.borderStyle = .roundedRect
        
        stack.addArrangedSubview(testLabel)
        stack.addArrangedSubview(packageSizeTextField)
        
        contentView.addSubview(stack)
        
        let g = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([
            stack.topAnchor.constraint(equalTo: g.topAnchor),
            stack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            stack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            stack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
        
        packageSizeTextField.addTarget(self, action: #selector(textChanged(_:)), for: .editingChanged)
    }
    
    var changeClosure: ((String, UITableViewCell)->())?
    
    @objc func textChanged(_ v: UITextField) -> Void {
        print(#function)
        changeClosure?(v.text ?? "", self)
    }
}

TableView 控制器 Class

class MyTableViewController: UITableViewController {
    
    lazy var myTableViewDataSource: MyTableViewDataSource = {
        MyTableViewDataSource()
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        tableView.register(MyTableViewCell.self, forCellReuseIdentifier: "mtvc")
        tableView.dataSource = myTableViewDataSource
    }

}

TableView 数据源Class

final class MyTableViewDataSource: NSObject, UITableViewDataSource {

    var myData: [String] = [
        " ",
        "One",
        "Two",
        "Three",
        "Four",
        "Five",
        "Six",
        "Seven",
        "Eight",
    ]

    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            myData.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .fade)
            tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
        } else if editingStyle == .insert {
            // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
        }
    }
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return indexPath.row != 0
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myData.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let c = tableView.dequeueReusableCell(withIdentifier: "mtvc", for: indexPath) as! MyTableViewCell
        c.testLabel.isHidden = indexPath.row != 0
        c.packageSizeTextField.isHidden = indexPath.row == 0

        if indexPath.row == 0 {
            myData[0] = myData.dropFirst().joined(separator: " : ")
            c.testLabel.text = myData[indexPath.row]
        } else {
            c.packageSizeTextField.text = myData[indexPath.row]
        }
        
        c.changeClosure = { [weak self, weak tableView] str, c in
            guard let self = self,
                  let tableView = tableView,
                  let pth = tableView.indexPath(for: c)
            else {
                return
            }
            // update our data
            self.myData[pth.row] = str
            // do something with the tableView
            //  such as reload the first row (row Zero)
            tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
        }
        return c
    }
}

编辑 2

有很多内容需要讨论,但超出了您的问题范围,但简而言之...

首先,作为一般规则 Classes应该尽可能独立。

  • 你的 Cell 应该只处理它的元素
  • 您的数据源应该只管理数据(当然还有必要的资金,如返回单元格、处理编辑提交等)
  • 您的 TableViewController 应该,正如预期的那样,控制 tableView

如果您只是操作数据并想重新加载特定的行,对于您的 DataSource class 获取对 tableView 的引用并不是什么大不了的事情。

但是,如果您需要做的不止于此怎么办?例如:

您不希望您的 Cell 或 DataSource class 对按钮点击进行操作并执行类似将新控制器推到导航堆栈的操作。

要使用协议/委托模式,您可以通过classes“传递委托引用”。

这是一个例子(只有最少的代码)...

两种协议 - 一种用于文本更改,一种用于按钮点击:

protocol MyTextChangeDelegate: AnyObject {
    func cellTextChanged(_ cell: UITableViewCell)
}

protocol MyButtonTapDelegate: AnyObject {
    func cellButtonTapped(_ cell: UITableViewCell)
}

控制器class,符合MyButtonTapDelegate:

class TheTableViewController: UITableViewController, MyButtonTapDelegate {
    
    lazy var myTableViewDataSource: TheTableViewDataSource = {
        TheTableViewDataSource()
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // assign custom delegate to dataSource instance
        myTableViewDataSource.theButtonTapDelegate = self
        tableView.dataSource = myTableViewDataSource
    }

    // delegate func
    func cellButtonTapped(_ cell: UITableViewCell) {
        // do something
    }
    
}

数据源class,符合MyTextChangeDelegate,并引用了MyButtonTapDelegate以“传递给单元格”:

final class TheTableViewDataSource: NSObject, UITableViewDataSource, MyTextChangeDelegate {
    
    weak var theButtonTapDelegate: MyButtonTapDelegate?
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! theCell
        // assign custom delegate to cell instance
        c.theTextChangeDelegate = self
        c.theButtonTapDelegate = self.theButtonTapDelegate
        return c
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }
    
    func cellTextChanged(_ cell: UITableViewCell) {
        // update the data
    }

}

和 Cell class,它将在文本更改时调用 MyTextChangeDelegate(数据源 class),以及 MyButtonTapDelegate(控制器 class) 当点击按钮时:

final class theCell: UITableViewCell, UITextFieldDelegate {

    weak var theTextChangeDelegate: MyTextChangeDelegate?
    weak var theButtonTapDelegate: MyButtonTapDelegate?

    func textFieldDidChangeSelection(_ textField: UITextField) {
        theTextChangeDelegate?.cellTextChanged(self)
    }
    func buttonTapped() {
        theButtonTapDelegate?.cellButtonTapped(self)
    }

}

所以,说了这么多...

抽象地讲可能很困难。对于您的具体实施,您可能正在给自己挖坑。

您提到 “如何使用容器视图/分段控件在控制器之间切换” ... 可能 更好地创建“数据管理器”class,而不是“数据源”class。

此外,稍微搜索一下 Swift Closure vs Delegate,您会发现很多讨论都指出闭包是当今的首选方法。

我在 GitHub 上放了一个项目来展示这两种方法。功能是相同的——一种方法使用闭包,另一种方法使用 Protocol/Delegate 模式。您可以查看并深入研究代码(尽量保持直截了当),看看哪个更适合您。

https://github.com/DonMag/DelegatesAndClosures