我正在努力将 UITableViewDataSource 移出 UITableViewController。但是我有一些自定义单元格有自己的委托,然后它们会调用 tableView 进行重新加载。我...
我正在努力移动 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
}
一种选择是让您 MyTableViewController
符合您的要求 MyTableViewCellDelegate
在您的 dataSource 类中将 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 ?? "")
}
}
现在,在您的 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 连接即可运行的完整示例。
该单元格创建一个标签和一个文本字段,并将它们排列在垂直堆栈视图中。
零行将隐藏文本字段,并将其标签文本设置为来自的连接字符串 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
有很多内容需要讨论,超出了你的问题的范围,但简而言之......
首先, 一般来说, 类别应该尽可能独立。
如果您只是操作数据并希望重新加载特定的行,那么您的 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
@DonMag 一如既往地感谢您的深入回复。这可能需要我几天时间才能解决。我不太了解闭包的语法,而且我有相当多不同的单元格需要处理……虽然问题似乎真的是我缺乏理解,并且可能想在 dataSource 中做太多事情。看来我最好把所有东西都留在控制器中,然后弄清楚如何使用 containerView / 分段控件在控制器之间切换,因为这似乎是我在本例中进行分离的主要目的。