【问题标题】:Correct way to use UITableViewDataSource and Custom UITableViewCell Delegate使用 UITableViewDataSource 和自定义 UITableViewCell 委托的正确方法
【发布时间】:2021-09-06 08:45:41
【问题描述】:

我正在努力将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
    }
    
}

我的问题是,我如何访问该课程为其提供源的tableView?像这样添加对tableView的引用就这么简单吗?

var tableView: UITableView
var myProperty: MyProperty!

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

【问题讨论】:

  • 通常,UITableViewDatasource 方法有一个UITableView 作为参数。为什么不将它也作为方法的参数传递呢?
  • @Larme 那是我想到的另一个解决方案。添加var tableView: UITableView 作为单元格本身的属性。 - 我只是不确定从UITableViewCell 访问 tableView 的最佳/最正确方法是什么。

标签: ios swift uitableview uiviewcontroller


【解决方案1】:

一种选择是让您的MyTableViewController 符合您的MyTableViewCellDelegate,然后在您的dataSource 类中将控制器设置为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 ?? "")
    }
}

现在,在你的 dataSource 类中,设置闭包:

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
}

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


编辑

这是一个无需任何 Storyboard 连接即可运行的完整示例。

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

第 0 行将隐藏文本字段,其标签文本将设置为来自 myData 的串联字符串。

其余行将隐藏标签。

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

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

当任何行的文本字段中的文本发生更改以及删除一行时,将重新加载第一行。

细胞类

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 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 数据源类

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

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

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

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

如果您只是在操作数据并希望重新加载特定的行,那么您的 DataSource 类获取对 tableView 的引用并不是什么大问题。

但是,如果您需要做更多的事情怎么办?例如:

您不希望您的 Cell 或 DataSource 类对按钮点击采取行动,并执行诸如将新控制器推送到导航堆栈之类的操作。

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

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

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

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

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

控制器类,符合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
    }
    
}

数据源类,符合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 类,它将在文本更改时调用MyTextChangeDelegate(数据源类),在点击按钮时调用MyButtonTapDelegate(控制器类):

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

}

所以,说了这么多……

抽象地表达可能很困难。对于您的特定实施,您可能正在自掘坟墓。

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

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

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

https://github.com/DonMag/DelegatesAndClosures

【讨论】:

  • 不要让单元格捕获索引路径。如果您在此之前插入或删除单元格。它需要引用此单元格的模型,然后在更改时让 tvc 查找相应单元格的索引路径
  • @Shadowrun - 好点...我编辑了我的答案以反映这一点。
  • @DonMag 感谢您一如既往的深入回应。这可能需要我几天的时间来完成。我不太了解闭包的语法,而且我有很多不同的单元格可以使用......虽然问题似乎真的是我缺乏理解,并且可能希望在数据源中做太多事情.似乎我最好将其全部留在控制器中并弄清楚如何使用 containerView / 分段控件在控制器之间切换,因为这似乎是我在这种情况下分离的主要目标。
  • @Shadowrun 是否有更好的方法来确定其委托函数中单元格的 indexPath? - 捕获 indexPath 的单元格都是静止的,并且不在可以移动/删除的部分中。它的唯一功能是在开关盒中用于方便目的,以便我知道我正在访问正确的单元格,例如存在多个相同单元格类型的情况
  • @StonedStudio - “一个选择是让您的 MyTableViewController 符合您的 MyTableViewCellDelegate,然后将控制器设置为 dataSource 类中 cellForRowAt 中的委托。” ...采取看看我的回答中的 Edit 2,以及我在 GitHub 上提出的一个快速项目。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多