问题描述
我已经使用新的UICollectionView
API通过自定义UICollectionViewCell
和UIContentConfiguration
自定义实现了iOS 14
列表。我一直在关注本教程:https://swiftsenpai.com/development/uicollectionview-list-custom-cell/(与Apple的示例项目一起)
基本上,您现在有了UICollectionViewCell
,UIContentConfiguration
和UIContentView
。 cell
仅设置其配置,content configuration
保存该单元及其所有可能状态的数据,而content view
是替换{{1 }}。
我让它正常工作,而且非常干净。但是有一件事我不明白:
您将如何向UIView
添加回调,或如何将单元格中所做的更改(例如,UICollectionViewCell.contentView
切换或UIContentView
更改)传达给UISwitch
?创建UITextField
的数据源时,viewController
和单元格之间的唯一连接在单元格注册内:
viewController
这是我能想到的唯一可以放置此类连接的地方,如上例所示。但是,这不起作用,因为该单元格不再对其内容负责。必须将此闭包传递到为单元格创建实际视图的collectionView
。
单元格及其内容视图之间的唯一连接是内容配置,但不能将闭包作为属性,因为它们不相等。所以我无法建立连接。
有人知道怎么做吗?
谢谢!
解决方法
如果您正在编写自己的配置,则由其负责。因此,请让您的配置定义协议并为其赋予delegate
属性!单元格注册对象将视图控制器(或任何人)设置为配置的委托。内容视图配置UISwitch或要向其发送信号的任何内容,然后内容视图将该信号传递给配置的委托。
工作示例
这是一个工作示例的 complete 代码。我选择使用表格视图而不是集合视图,但这是完全不相关的。内容配置适用于两者。
您需要做的就是将表视图放入视图控制器中,使视图控制器成为表视图的数据源,并使表视图成为视图控制器的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
}
}
}
}
该示例的关键部分
好的,可能看起来很多,但是对于带有自定义内容配置的任何表视图来说,它几乎都是纯样板。唯一有趣的部分是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
}
}
}
一种替代方法
该答案仍然使用协议和委托体系结构,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
,
您仍然可以使用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
}
,
所以我认为我想出了一种不使用委托的替代解决方案。
在此示例中,我有一个数据模型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
}
}
这会将配置部分完全保留在单元格内部,并且只是将相关内容公开给视图控制器中的单元格注册过程。
我不完全确定这是最好的方法,但它似乎现在可以正常工作。