正确使用 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
中的委托=]
但是,使用闭包可能会好得多。
删除单元格中的 delegate
和 indexPath
属性,并添加闭包 属性:
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 模式。您可以查看并深入研究代码(尽量保持直截了当),看看哪个更适合您。
我正在努力将 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
中的委托=]
但是,使用闭包可能会好得多。
删除单元格中的 delegate
和 indexPath
属性,并添加闭包 属性:
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 模式。您可以查看并深入研究代码(尽量保持直截了当),看看哪个更适合您。