6

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

基本上你现在有 a UICollectionViewCell, aUIContentConfiguration和 a UIContentViewcell仅设置其配置,保存content configuration单元格及其所有可能状态的数据,而content view是实际UIView替换UICollectionViewCell.contentView.

我让它工作了,它非常棒而且干净。但是有一点我不明白:

您将如何添加回调到UIContentView,或将单元格中所做的更改(例如UISwitch切换或UITextField更改)传达给viewController?和 cell之间的唯一联系是在创建的数据源viewController时在 cell 注册内部: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为单元格创建实际视图的那个。

单元格与其内容视图之间的唯一联系是内容配置,但不能将闭包作为属性,因为它们不相等。所以我无法建立连接。

有谁知道如何做到这一点?

谢谢!

4

3 回答 3

4

如果您正在编写自己的配置,则您负责其属性。所以让你的配置定义一个协议并给它一个delegate属性!单元注册对象将视图控制器(或任何人)设置为配置的委托。内容视图配置 UISwitch 或任何向它发出信号的内容视图,内容视图将该信号传递给配置的委托。

一个工作示例

这是一个工作示例的完整代码。我选择使用表格视图而不是集合视图,但这完全不相关;内容配置适用于两者。

您需要做的就是在视图控制器中放置一个表视图,使视图控制器成为表视图的数据源,并使表视图成为视图控制器的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
于 2020-09-24T18:01:32.903 回答
1

所以我想我想出了一个不使用代表的替代解决方案。

对于此示例,我有一个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
    }
    
}

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

我不完全确定这是最好的方法,但它现在似乎有效。

于 2020-09-25T10:46:11.843 回答
0

您仍然可以使用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
}
于 2020-09-24T17:56:16.280 回答