具有自定义配置的 UICollectionView 列表 - 如何将单元格中的更改传递给视图控制器?
UICollectionView List With Custom Configuration - how to pass changes in cell to view controller?
我已经使用新的 iOS 14
API 实现了一个 UICollectionView
列表,其中包含自定义 UICollectionViewCell
和 UIContentConfiguration
。我一直在学习本教程:https://swiftsenpai.com/development/uicollectionview-list-custom-cell/(以及 Apple 的示例项目)
基本上你现在有一个 UICollectionViewCell
、一个 UIContentConfiguration
和一个 UIContentView
。 cell
仅设置其配置,content configuration
保存单元格及其所有可能状态的数据,content view
是替换 [=22] 的实际 UIView
=].
我让它工作了,它非常棒而且干净。但是有一件事我不明白:
您将如何向 UIContentView
添加回调,或者如何将单元格中所做的更改(例如 UISwitch
切换或 UITextField
更改)传达给 viewController
? viewController
和单元格之间的唯一联系是在创建 collectionView
的数据源时单元格注册内部:
// Cell
class Cell: UICollectionViewListCell {
var event: Event?
var onEventDidChange: ((_ event: Event) -> Void)?
//...
}
// Example cell registration in ViewController
let eventCellRegistration = UICollectionView.CellRegistration<Event.Cell, Event> { [weak self] (cell, indexPath, event) in
cell.event = event // Setting the data model for the cell
// This is what I tried to do. A closure that the cell calls, whenever the cell made changes to the event (the model)
cell.onEventDidChange = { event in /* update database */ }
}
这是我能想到的唯一可以放置此类连接的地方,如上例所示。但是,这不起作用,因为单元格不再对其内容负责。此闭包必须传递给正在为单元格创建实际视图的 UIContentView
。
单元格与其内容视图之间的唯一联系是内容配置,但不能将闭包作为属性,因为它们不是等式的。所以我无法建立连接。
有人知道怎么做吗?
谢谢!
您仍然可以使用 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
为单元格设置委托,只是您不必再创建它:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let model = SOME_MODEL
let cell = collectionView.dequeueConfiguredReusableCell(using: eventCellRegistration,
for: indexPath,
item: model)
cell.delegate = self
return cell
}
如果您正在编写自己的配置,则您负责其属性。所以让你的配置定义一个协议并给它一个 delegate
属性!单元格注册对象将视图控制器(或任何人)设置为配置的委托。内容视图配置 UISwitch 或任何向它发送信号的内容视图,内容视图将该信号传递给配置的委托。
一个工作示例
这是工作示例的完整代码。我选择使用 table 视图而不是集合视图,但这完全无关紧要;内容配置适用于两者。
您需要做的就是在视图控制器中放置一个 table 视图,使视图控制器成为 table 视图的数据源,并使 table 视图成为视图控制器的数据源tableView
.
extension UIResponder {
func next<T:UIResponder>(ofType: T.Type) -> T? {
let r = self.next
if let r = r as? T ?? r?.next(ofType: T.self) {
return r
} else {
return nil
}
}
}
protocol SwitchListener : AnyObject {
func switchChangedTo(_:Bool, sender:UIView)
}
class MyContentView : UIView, UIContentView {
var configuration: UIContentConfiguration {
didSet {
config()
}
}
let sw = UISwitch()
init(configuration: UIContentConfiguration) {
self.configuration = configuration
super.init(frame:.zero)
sw.translatesAutoresizingMaskIntoConstraints = true
self.addSubview(sw)
sw.center = CGPoint(x:self.bounds.midX, y:self.bounds.midY)
sw.autoresizingMask = [.flexibleTopMargin, .flexibleBottomMargin, .flexibleLeftMargin, .flexibleRightMargin]
sw.addAction(UIAction {[unowned sw] action in
(configuration as? Config)?.delegate?.switchChangedTo(sw.isOn, sender:self)
}, for: .valueChanged)
config()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func config() {
self.sw.isOn = (configuration as? Config)?.isOn ?? false
}
}
struct Config: UIContentConfiguration {
var isOn = false
weak var delegate : SwitchListener?
func makeContentView() -> UIView & UIContentView {
return MyContentView(configuration:self)
}
func updated(for state: UIConfigurationState) -> Config {
return self
}
}
class ViewController: UIViewController, UITableViewDataSource {
@IBOutlet var tableView : UITableView!
var list = Array(repeating: false, count: 100)
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.list.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
var config = Config()
config.isOn = list[indexPath.row]
config.delegate = self
cell.contentConfiguration = config
return cell
}
}
extension ViewController : SwitchListener {
func switchChangedTo(_ newValue: Bool, sender: UIView) {
if let cell = sender.next(ofType: UITableViewCell.self) {
if let ip = self.tableView.indexPath(for: cell) {
self.list[ip.row] = newValue
}
}
}
}
该示例的关键部分
好吧,它可能看起来很多,但对于任何具有自定义内容配置的 table 视图来说,它大部分都是纯样板文件。唯一有趣的部分是 SwitchListener 协议及其实现,以及内容视图初始化程序中的 addAction
行;这就是这个答案的第一段描述的内容。
因此,在内容视图的初始化程序中:
sw.addAction(UIAction {[unowned sw] action in
(configuration as? Config)?.delegate?.switchChangedTo(sw.isOn, sender:self)
}, for: .valueChanged)
并且在扩展中,响应该调用的方法:
func switchChangedTo(_ newValue: Bool, sender: UIView) {
if let cell = sender.next(ofType: UITableViewCell.self) {
if let ip = self.tableView.indexPath(for: cell) {
self.list[ip.row] = newValue
}
}
}
另一种方法
该答案仍然使用 protocol-and-delegate 架构,OP 宁愿不这样做。现代的方法是提供一个 属性 ,其值是一个函数,可以直接调用 .
所以我们没有给我们的配置一个委托,而是给它一个回调 属性:
struct Config: UIContentConfiguration {
var isOn = false
var isOnChanged : ((Bool, UIView) -> Void)?
内容视图的初始化程序配置界面元素,以便在它发出信号时调用 isOnChanged
函数:
sw.addAction(UIAction {[unowned sw] action in
(configuration as? Config)?.isOnChanged?(sw.isOn, self)
}, for: .valueChanged)
它只是为了显示 isOnChanged
函数 是什么 。在我的示例中,它与之前架构中的委托方法完全相同。所以,当我们配置单元格时:
config.isOn = list[indexPath.row]
config.isOnChanged = { [weak self] isOn, v in
if let cell = v.next(ofType: UITableViewCell.self) {
if let ip = self?.tableView.indexPath(for: cell) {
self?.list[ip.row] = isOn
}
}
}
cell.contentConfiguration = config
所以我想我想出了一个不使用委托的替代解决方案。
对于这个例子,我有一个数据模型 Event
只包含年份和名字,collectionView
只显示所有事件:
struct Event: Identifiable, Codable, Hashable {
let id: UUID
var date: Date
var name: String
var year: Int { ... }
//...
}
extension Event {
// The collection view cell
class Cell: UICollectionViewListCell {
// item is an abstraction to the event type. In there, you can put closures that the cell can call
var item: ContentConfiguration.Item?
override func updateConfiguration(using state: UICellConfigurationState) {
let newBackgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
backgroundConfiguration = newBackgroundConfiguration
var newConfiguration = Event.ContentConfiguration().updated(for: state)
// Assign the item to the new configuration
newConfiguration.item = item
contentConfiguration = newConfiguration
}
}
struct ContentConfiguration: UIContentConfiguration, Hashable {
/// The view model associated with the configuration. It handles the data that the cell holds but is not responsible for stuff like `nameColor`, which goes directly into the configuration struct.
struct Item: Identifiable, Hashable {
var id = UUID()
var event: Event? = nil
var onNameChanged: ((_ newName: String) -> Void)? = nil
var isDraft: Bool = false
// This is needed for being Hashable. You should never modify an Item, simply create a new instance every time. That's fast because it's a struct.
static func == (lhs: Item, rhs: Item) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
/// The associated view model item.
var item: Item?
// Other stuff the configuration is handling
var nameColor: UIColor?
var nameEditable: Bool?
func makeContentView() -> UIView & UIContentView {
ContentView(configuration: self)
}
func updated(for state: UIConfigurationState) -> Event.ContentConfiguration {
guard let state = state as? UICellConfigurationState else { return self }
var updatedConfiguration = self
// Example state-based change to switch out the label with a text field
if state.isSelected {
updatedConfiguration.nameEditable = true
} else {
updatedConfiguration.nameEditable = false
}
return updatedConfiguration
}
}
// Example content view. Simply showing the year and name
class ContentView: UIView, UIContentView, UITextFieldDelegate {
private var appliedConfiguration: Event.ContentConfiguration!
var configuration: UIContentConfiguration {
get {
appliedConfiguration
}
set {
guard let newConfiguration = newValue as? Event.ContentConfiguration else {
return
}
apply(configuration: newConfiguration)
}
}
let yearLabel: UILabel = UILabel()
let nameLabel: UILabel = UILabel()
let nameTextField: UITextField = UITextField()
init(configuration: Event.ContentConfiguration) {
super.init(frame: .zero)
setupInternalViews()
apply(configuration: configuration)
}
required init?(coder: NSCoder) {
fatalError()
}
private func setupInternalViews() {
addSubview(yearLabel)
addSubview(nameLabel)
addSubview(nameTextField)
nameTextField.borderStyle = .roundedRect
nameTextField.delegate = self
yearLabel.textAlignment = .center
yearLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.translatesAutoresizingMaskIntoConstraints = false
yearLabel.snp.makeConstraints { (make) in
make.leading.equalToSuperview().offset(12)
make.top.equalToSuperview().offset(12)
make.bottom.equalToSuperview().offset(-12)
make.width.equalTo(80)
}
nameLabel.snp.makeConstraints { (make) in
make.leading.equalTo(yearLabel.snp.trailing).offset(10)
make.top.equalToSuperview().offset(12)
make.bottom.equalToSuperview().offset(-12)
make.trailing.equalToSuperview().offset(-12)
}
nameTextField.snp.makeConstraints { (make) in
make.leading.equalTo(yearLabel.snp.trailing).offset(10)
make.top.equalToSuperview().offset(12)
make.bottom.equalToSuperview().offset(-12)
make.trailing.equalToSuperview().offset(-12)
}
}
/// Apply a new configuration.
/// - Parameter configuration: The new configuration
private func apply(configuration: Event.ContentConfiguration) {
guard appliedConfiguration != configuration else { return }
appliedConfiguration = configuration
yearLabel.text = String(configuration.item?.event?.year ?? 0)
nameLabel.text = configuration.item?.event?.name
nameLabel.textColor = configuration.nameColor
if configuration.nameEditable == true {
nameLabel.isHidden = true
nameTextField.isHidden = false
nameTextField.text = configuration.item?.event?.name
} else {
nameLabel.isHidden = false
nameTextField.isHidden = true
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
// Simply use the item to call the given closure
appliedConfiguration.item?.onNameChanged?(nameTextField.text ?? "")
return true
}
}
}
单元格注册如下所示:
let eventCellRegistration = UICollectionView.CellRegistration<Event.Cell, Event> { [weak self] (cell, indexPath, event) in
var item = Event.ContentConfiguration.Item()
item.event = event
item.onNameChanged = { [weak self] newName in
// Do what you need to do with the changed value, i.e. send it to your data provider in order to update the database with the changed data
}
}
这将配置部分完全保留在单元格中,并且只将相关内容公开给视图控制器中的单元格注册过程。
我不完全确定这是最好的方法,但它现在似乎有效。
我已经使用新的 iOS 14
API 实现了一个 UICollectionView
列表,其中包含自定义 UICollectionViewCell
和 UIContentConfiguration
。我一直在学习本教程:https://swiftsenpai.com/development/uicollectionview-list-custom-cell/(以及 Apple 的示例项目)
基本上你现在有一个 UICollectionViewCell
、一个 UIContentConfiguration
和一个 UIContentView
。 cell
仅设置其配置,content configuration
保存单元格及其所有可能状态的数据,content view
是替换 [=22] 的实际 UIView
=].
我让它工作了,它非常棒而且干净。但是有一件事我不明白:
您将如何向 UIContentView
添加回调,或者如何将单元格中所做的更改(例如 UISwitch
切换或 UITextField
更改)传达给 viewController
? viewController
和单元格之间的唯一联系是在创建 collectionView
的数据源时单元格注册内部:
// Cell
class Cell: UICollectionViewListCell {
var event: Event?
var onEventDidChange: ((_ event: Event) -> Void)?
//...
}
// Example cell registration in ViewController
let eventCellRegistration = UICollectionView.CellRegistration<Event.Cell, Event> { [weak self] (cell, indexPath, event) in
cell.event = event // Setting the data model for the cell
// This is what I tried to do. A closure that the cell calls, whenever the cell made changes to the event (the model)
cell.onEventDidChange = { event in /* update database */ }
}
这是我能想到的唯一可以放置此类连接的地方,如上例所示。但是,这不起作用,因为单元格不再对其内容负责。此闭包必须传递给正在为单元格创建实际视图的 UIContentView
。
单元格与其内容视图之间的唯一联系是内容配置,但不能将闭包作为属性,因为它们不是等式的。所以我无法建立连接。
有人知道怎么做吗?
谢谢!
您仍然可以使用 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
为单元格设置委托,只是您不必再创建它:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let model = SOME_MODEL
let cell = collectionView.dequeueConfiguredReusableCell(using: eventCellRegistration,
for: indexPath,
item: model)
cell.delegate = self
return cell
}
如果您正在编写自己的配置,则您负责其属性。所以让你的配置定义一个协议并给它一个 delegate
属性!单元格注册对象将视图控制器(或任何人)设置为配置的委托。内容视图配置 UISwitch 或任何向它发送信号的内容视图,内容视图将该信号传递给配置的委托。
一个工作示例
这是工作示例的完整代码。我选择使用 table 视图而不是集合视图,但这完全无关紧要;内容配置适用于两者。
您需要做的就是在视图控制器中放置一个 table 视图,使视图控制器成为 table 视图的数据源,并使 table 视图成为视图控制器的数据源tableView
.
extension UIResponder {
func next<T:UIResponder>(ofType: T.Type) -> T? {
let r = self.next
if let r = r as? T ?? r?.next(ofType: T.self) {
return r
} else {
return nil
}
}
}
protocol SwitchListener : AnyObject {
func switchChangedTo(_:Bool, sender:UIView)
}
class MyContentView : UIView, UIContentView {
var configuration: UIContentConfiguration {
didSet {
config()
}
}
let sw = UISwitch()
init(configuration: UIContentConfiguration) {
self.configuration = configuration
super.init(frame:.zero)
sw.translatesAutoresizingMaskIntoConstraints = true
self.addSubview(sw)
sw.center = CGPoint(x:self.bounds.midX, y:self.bounds.midY)
sw.autoresizingMask = [.flexibleTopMargin, .flexibleBottomMargin, .flexibleLeftMargin, .flexibleRightMargin]
sw.addAction(UIAction {[unowned sw] action in
(configuration as? Config)?.delegate?.switchChangedTo(sw.isOn, sender:self)
}, for: .valueChanged)
config()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func config() {
self.sw.isOn = (configuration as? Config)?.isOn ?? false
}
}
struct Config: UIContentConfiguration {
var isOn = false
weak var delegate : SwitchListener?
func makeContentView() -> UIView & UIContentView {
return MyContentView(configuration:self)
}
func updated(for state: UIConfigurationState) -> Config {
return self
}
}
class ViewController: UIViewController, UITableViewDataSource {
@IBOutlet var tableView : UITableView!
var list = Array(repeating: false, count: 100)
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.list.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
var config = Config()
config.isOn = list[indexPath.row]
config.delegate = self
cell.contentConfiguration = config
return cell
}
}
extension ViewController : SwitchListener {
func switchChangedTo(_ newValue: Bool, sender: UIView) {
if let cell = sender.next(ofType: UITableViewCell.self) {
if let ip = self.tableView.indexPath(for: cell) {
self.list[ip.row] = newValue
}
}
}
}
该示例的关键部分
好吧,它可能看起来很多,但对于任何具有自定义内容配置的 table 视图来说,它大部分都是纯样板文件。唯一有趣的部分是 SwitchListener 协议及其实现,以及内容视图初始化程序中的 addAction
行;这就是这个答案的第一段描述的内容。
因此,在内容视图的初始化程序中:
sw.addAction(UIAction {[unowned sw] action in
(configuration as? Config)?.delegate?.switchChangedTo(sw.isOn, sender:self)
}, for: .valueChanged)
并且在扩展中,响应该调用的方法:
func switchChangedTo(_ newValue: Bool, sender: UIView) {
if let cell = sender.next(ofType: UITableViewCell.self) {
if let ip = self.tableView.indexPath(for: cell) {
self.list[ip.row] = newValue
}
}
}
另一种方法
该答案仍然使用 protocol-and-delegate 架构,OP 宁愿不这样做。现代的方法是提供一个 属性 ,其值是一个函数,可以直接调用 .
所以我们没有给我们的配置一个委托,而是给它一个回调 属性:
struct Config: UIContentConfiguration {
var isOn = false
var isOnChanged : ((Bool, UIView) -> Void)?
内容视图的初始化程序配置界面元素,以便在它发出信号时调用 isOnChanged
函数:
sw.addAction(UIAction {[unowned sw] action in
(configuration as? Config)?.isOnChanged?(sw.isOn, self)
}, for: .valueChanged)
它只是为了显示 isOnChanged
函数 是什么 。在我的示例中,它与之前架构中的委托方法完全相同。所以,当我们配置单元格时:
config.isOn = list[indexPath.row]
config.isOnChanged = { [weak self] isOn, v in
if let cell = v.next(ofType: UITableViewCell.self) {
if let ip = self?.tableView.indexPath(for: cell) {
self?.list[ip.row] = isOn
}
}
}
cell.contentConfiguration = config
所以我想我想出了一个不使用委托的替代解决方案。
对于这个例子,我有一个数据模型 Event
只包含年份和名字,collectionView
只显示所有事件:
struct Event: Identifiable, Codable, Hashable {
let id: UUID
var date: Date
var name: String
var year: Int { ... }
//...
}
extension Event {
// The collection view cell
class Cell: UICollectionViewListCell {
// item is an abstraction to the event type. In there, you can put closures that the cell can call
var item: ContentConfiguration.Item?
override func updateConfiguration(using state: UICellConfigurationState) {
let newBackgroundConfiguration = UIBackgroundConfiguration.listGroupedCell()
backgroundConfiguration = newBackgroundConfiguration
var newConfiguration = Event.ContentConfiguration().updated(for: state)
// Assign the item to the new configuration
newConfiguration.item = item
contentConfiguration = newConfiguration
}
}
struct ContentConfiguration: UIContentConfiguration, Hashable {
/// The view model associated with the configuration. It handles the data that the cell holds but is not responsible for stuff like `nameColor`, which goes directly into the configuration struct.
struct Item: Identifiable, Hashable {
var id = UUID()
var event: Event? = nil
var onNameChanged: ((_ newName: String) -> Void)? = nil
var isDraft: Bool = false
// This is needed for being Hashable. You should never modify an Item, simply create a new instance every time. That's fast because it's a struct.
static func == (lhs: Item, rhs: Item) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
/// The associated view model item.
var item: Item?
// Other stuff the configuration is handling
var nameColor: UIColor?
var nameEditable: Bool?
func makeContentView() -> UIView & UIContentView {
ContentView(configuration: self)
}
func updated(for state: UIConfigurationState) -> Event.ContentConfiguration {
guard let state = state as? UICellConfigurationState else { return self }
var updatedConfiguration = self
// Example state-based change to switch out the label with a text field
if state.isSelected {
updatedConfiguration.nameEditable = true
} else {
updatedConfiguration.nameEditable = false
}
return updatedConfiguration
}
}
// Example content view. Simply showing the year and name
class ContentView: UIView, UIContentView, UITextFieldDelegate {
private var appliedConfiguration: Event.ContentConfiguration!
var configuration: UIContentConfiguration {
get {
appliedConfiguration
}
set {
guard let newConfiguration = newValue as? Event.ContentConfiguration else {
return
}
apply(configuration: newConfiguration)
}
}
let yearLabel: UILabel = UILabel()
let nameLabel: UILabel = UILabel()
let nameTextField: UITextField = UITextField()
init(configuration: Event.ContentConfiguration) {
super.init(frame: .zero)
setupInternalViews()
apply(configuration: configuration)
}
required init?(coder: NSCoder) {
fatalError()
}
private func setupInternalViews() {
addSubview(yearLabel)
addSubview(nameLabel)
addSubview(nameTextField)
nameTextField.borderStyle = .roundedRect
nameTextField.delegate = self
yearLabel.textAlignment = .center
yearLabel.translatesAutoresizingMaskIntoConstraints = false
nameLabel.translatesAutoresizingMaskIntoConstraints = false
yearLabel.snp.makeConstraints { (make) in
make.leading.equalToSuperview().offset(12)
make.top.equalToSuperview().offset(12)
make.bottom.equalToSuperview().offset(-12)
make.width.equalTo(80)
}
nameLabel.snp.makeConstraints { (make) in
make.leading.equalTo(yearLabel.snp.trailing).offset(10)
make.top.equalToSuperview().offset(12)
make.bottom.equalToSuperview().offset(-12)
make.trailing.equalToSuperview().offset(-12)
}
nameTextField.snp.makeConstraints { (make) in
make.leading.equalTo(yearLabel.snp.trailing).offset(10)
make.top.equalToSuperview().offset(12)
make.bottom.equalToSuperview().offset(-12)
make.trailing.equalToSuperview().offset(-12)
}
}
/// Apply a new configuration.
/// - Parameter configuration: The new configuration
private func apply(configuration: Event.ContentConfiguration) {
guard appliedConfiguration != configuration else { return }
appliedConfiguration = configuration
yearLabel.text = String(configuration.item?.event?.year ?? 0)
nameLabel.text = configuration.item?.event?.name
nameLabel.textColor = configuration.nameColor
if configuration.nameEditable == true {
nameLabel.isHidden = true
nameTextField.isHidden = false
nameTextField.text = configuration.item?.event?.name
} else {
nameLabel.isHidden = false
nameTextField.isHidden = true
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
// Simply use the item to call the given closure
appliedConfiguration.item?.onNameChanged?(nameTextField.text ?? "")
return true
}
}
}
单元格注册如下所示:
let eventCellRegistration = UICollectionView.CellRegistration<Event.Cell, Event> { [weak self] (cell, indexPath, event) in
var item = Event.ContentConfiguration.Item()
item.event = event
item.onNameChanged = { [weak self] newName in
// Do what you need to do with the changed value, i.e. send it to your data provider in order to update the database with the changed data
}
}
这将配置部分完全保留在单元格中,并且只将相关内容公开给视图控制器中的单元格注册过程。
我不完全确定这是最好的方法,但它现在似乎有效。