Lags in UICollectionView accessories when animatedly applying a snapshot unless .disclosureIndicator() is also an accessory

Please run the following UIKit app.

It displays a collection view with compositional layout (list layout) and diffable data source.

import UIKit

class ViewController: UIViewController {
    var bool = false {
        didSet {
            var snapshot = dataSource.snapshot()
            snapshot.reconfigureItems(snapshot.itemIdentifiers)
            dataSource.apply(snapshot, animatingDifferences: true)
        }
    }
    
    var collectionView: UICollectionView!
    
    var dataSource: UICollectionViewDiffableDataSource<String, String>!
    
    var snapshot: NSDiffableDataSourceSnapshot<String, String> {
        var snapshot = NSDiffableDataSourceSnapshot<String, String>()
        snapshot.appendSections(["section"])
        snapshot.appendItems(["id"])
        return snapshot
    }

    override func viewDidLoad() {
        super.viewDidLoad()
                        
        configureHierarchy()
        configureDataSource()
    }
    
    func configureHierarchy() {
        collectionView = .init(frame: view.bounds, collectionViewLayout: createLayout())
        view.addSubview(collectionView)
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    }
    
    func createLayout() -> UICollectionViewLayout {
        let configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        return UICollectionViewCompositionalLayout.list(using: configuration)
    }
    
    func configureDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { [weak self] cell, indexPath, itemIdentifier in
            guard let self else { return }
            
            let _switch = UISwitch()
            cell.accessories = [
                .customView(configuration: .init(
                    customView: _switch,
                    placement: .trailing())
                ),
//                .disclosureIndicator()
            ]
            _switch.isOn = bool
            _switch.addTarget(self, action: #selector(toggleBool), for: .valueChanged)
        }
        
        dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
            collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
        }
        
        dataSource.apply(self.snapshot, animatingDifferences: false)
    }
    
    @objc func toggleBool() {
        bool.toggle()
    }
}

When you tap on the switch, it lags.

If you uncomment .disclosureIndicator() and tap on the switch, it doesn't lag.

How do I make it so that the switch doesn't lag without having a disclosure indicator in the cell?

Note: while it would solve the issue, I would prefer not to declare the switch at the class level, as I don't want to declare all my controls, which could be quite a lot, at the view controller level in my real app.

Edit: declaring the switch at the configureDataSource() level also fixes it, but it would still be inconvenient to declare many switches, say of a list with n elements, at that level.

Accepted Reply

Make a custom cell:

class SwitchCell: UICollectionViewListCell {
    let _switch = UISwitch()

    override init(frame: CGRect) {
        super.init(frame: frame)
        
        accessories = [
            .customView(configuration: .init(
                customView: _switch,
                placement: .trailing())
            )
        ]
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

And edit cellRegistration accordingly:

let cellRegistration = UICollectionView.CellRegistration<SwitchCell, String> { [weak self] cell, indexPath, itemIdentifier in
    guard let self else { return }
    
    cell._switch.isOn = bool
    cell._switch.addTarget(self, action: #selector(toggleBool), for: .valueChanged)
}

Replies

Make a custom cell:

class SwitchCell: UICollectionViewListCell {
    let _switch = UISwitch()

    override init(frame: CGRect) {
        super.init(frame: frame)
        
        accessories = [
            .customView(configuration: .init(
                customView: _switch,
                placement: .trailing())
            )
        ]
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

And edit cellRegistration accordingly:

let cellRegistration = UICollectionView.CellRegistration<SwitchCell, String> { [weak self] cell, indexPath, itemIdentifier in
    guard let self else { return }
    
    cell._switch.isOn = bool
    cell._switch.addTarget(self, action: #selector(toggleBool), for: .valueChanged)
}