Search code examples
iosswiftuicollectionviewaccessoryuicollectionviewlistcell

Creating a UICellAccessory.customView instance and using it in multiple listCell accessories causes hang


I wanted to make a UI like this.

enter image description here

The component of the chevron down image on the right side of the cell is the UICellAccessory that I customized. From now on, I will call this moreAccessory.

To make this UI, I configured the dataSource with redCellRegistration and blueCellRegistration as follows.

let redCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
    var content = cell.defaultContentConfiguration()
    content.text = "\(item)"
    content.textProperties.color = .red
    cell.contentConfiguration = content
    let image = UIImageView(image: UIImage(systemName: "chevron.down"))
    let configuration = UICellAccessory.CustomViewConfiguration(customView: image,
                                                                placement: .trailing(),
                                                                tintColor: .systemGray)
    let moreAccessory = UICellAccessory.customView(configuration: configuration)
    cell.accessories = [moreAccessory]
}

let blueCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
    var content = cell.defaultContentConfiguration()
    content.text = "\(item)"
    content.textProperties.color = .blue
    cell.contentConfiguration = content
    let image = UIImageView(image: UIImage(systemName: "chevron.down"))
    let configuration = UICellAccessory.CustomViewConfiguration(customView: image,
                                                                placement: .trailing(),
                                                                tintColor: .systemGray)
    let moreAccessory = UICellAccessory.customView(configuration: configuration)
    cell.accessories = [moreAccessory]
}

dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) {
    (collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in
    
    if indexPath.row.isMultiple(of: 2) {
        return collectionView.dequeueConfiguredReusableCell(using: redCellRegistration, for: indexPath, item: identifier)
    } else {
        return collectionView.dequeueConfiguredReusableCell(using: blueCellRegistration, for: indexPath, item: identifier)
    }
}

And I recognized that there was a duplicate code for creating moreAccessory instance in redCellRegistration and blueCellRegistration, so decided to make this accessory a single variable and use it in common.

let moreAccessory: UICellAccessory = {
    let image = UIImageView(image: UIImage(systemName: "chevron.down"))
    let configuration = UICellAccessory.CustomViewConfiguration(customView: image,
                                                                placement: .trailing(),
                                                                tintColor: .systemGray)
    let accessory = UICellAccessory.customView(configuration: configuration)
    return accessory
}()


let redCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
    var content = cell.defaultContentConfiguration()
    content.text = "\(item)"
    content.textProperties.color = .red
    cell.contentConfiguration = content
    cell.accessories = [moreAccessory]
}

let blueCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
    var content = cell.defaultContentConfiguration()
    content.text = "\(item)"
    content.textProperties.color = .blue
    cell.contentConfiguration = content
    cell.accessories = [moreAccessory]
}

But the code above caused hang.

When looking at the Instruments tool, UICellAccessoryManager_updateAccessories:prevailingAccessories:withLayout:edge: function occupied too much weight.

enter image description here

The usage of cpu was 100%, and the memory usage also continued to go up.

However, if I create and use an accessory through a function that returns moreAccessory, there was no error in this situation.

func moreAccessory() -> UICellAccessory {
    let image = UIImageView(image: UIImage(systemName: "chevron.down"))
    let configuration = UICellAccessory.CustomViewConfiguration(customView: image,
                                                                placement: .trailing(),
                                                                tintColor: .systemGray)
    let accessory = UICellAccessory.customView(configuration: configuration)
    return accessory
}

let redCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
    var content = cell.defaultContentConfiguration()
    content.text = "\(item)"
    content.textProperties.color = .red
    cell.contentConfiguration = content
    cell.accessories = [moreAccessory()]
}

let blueCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
    var content = cell.defaultContentConfiguration()
    content.text = "\(item)"
    content.textProperties.color = .blue
    cell.contentConfiguration = content
    cell.accessories = [moreAccessory()]
}

I thought, "Well, was it a problem to use the same instance?". And assigned moreAccessory to the variables in redCellRegistration and blueCellRegistration separately. (Because the UICellAccessory is struct, if I assign it to a variable, the instance will be copied.)


let moreAccessory: UICellAccessory = {
    let image = UIImageView(image: UIImage(systemName: "chevron.down"))
    let configuration = UICellAccessory.CustomViewConfiguration(customView: image,
                                                                placement: .trailing(),
                                                                tintColor: .systemGray)
    let accessory = UICellAccessory.customView(configuration: configuration)
    return accessory
}()

let redCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
    var content = cell.defaultContentConfiguration()
    content.text = "\(item)"
    content.textProperties.color = .red
    cell.contentConfiguration = content
    let redCellAccessory = moreAccessory
    cell.accessories = [redCellAccessory]
}

let blueCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, Int> { (cell, indexPath, item) in
    var content = cell.defaultContentConfiguration()
    content.text = "\(item)"
    content.textProperties.color = .blue
    cell.contentConfiguration = content
    let blueCellAccessory = moreAccessory
    cell.accessories = [blueCellAccessory]
}

However, there was an error at this time, too.

And If I make moreAccessory as a predefined accessory such as disclosureIndicator, not customView, there was no error.

let moreAccessory: UICellAccessory = {
    let accessory = UICellAccessory.disclosureIndicator()
    return accessory
}()

enter image description here Is there anything to get a clue about this error situation?


Solution

  • UICellAccessory is a struct, but the UIView subclass it contains when creating a custom accessory is a class. When the struct is copied, the same view instance will be used; a new view will not be created. You cannot share the same single custom view (a UIImageView in your example) between cells.

    You have already found the correct solution: you need to make a new instance of UICellAccessory for every separate row if you are using a custom view. Your original code worked, but to avoid the duplication you can have a factory method to create a new cell accessory. That was your other solution that worked because it was creating a new instance each time it was called.