具有自定义配置的UICollectionView列表-如何将单元格中的更改传递给视图控制器?

问题描述

我已经使用新的UICollectionView API通过自定义UICollectionViewCellUIContentConfiguration自定义实现了iOS 14列表。我一直在关注本教程:https://swiftsenpai.com/development/uicollectionview-list-custom-cell/(与Apple的示例项目一起)

基本上,您现在有了UICollectionViewCellUIContentConfigurationUIContentViewcell仅设置其配置,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
    }
    
}

这会将配置部分完全保留在单元格内部,并且只是将相关内容公开给视图控制器中的单元格注册过程。

我不完全确定这是最好的方法,但它似乎现在可以正常工作。