Search code examples
uikituicollectionviewcompositionallayout

UICollectionViewListCell not resizing


Please run the following UIKit app.

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

The collection view has one section with one row.

The cell contains a text field that is pinned to its contentView.

import UIKit

class ViewController: UIViewController {
    var collectionView: UICollectionView!
    
    var dataSource: UICollectionViewDiffableDataSource<String, String>!

    override func viewDidLoad() {
        super.viewDidLoad()

        configureHierarchy()
        configureDataSource()
    }

    func configureHierarchy() {
        collectionView = .init(frame: .zero, collectionViewLayout: createLayout())
        view.addSubview(collectionView)
        collectionView.frame = view.bounds
    }
    
    func createLayout() -> UICollectionViewLayout {
        UICollectionViewCompositionalLayout { section, layoutEnvironment in
            let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
            return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
        }
    }
    
    func configureDataSource() {
        let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in
            let textField = UITextField()
            textField.placeholder = "Placeholder"
            textField.font = .systemFont(ofSize: 100)
            
            cell.contentView.addSubview(textField)
            textField.pinToSuperview()
        }
        
        dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
            collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
        }
        
        var snapshot = NSDiffableDataSourceSnapshot<String, String>()
        snapshot.appendSections(["main"])
        snapshot.appendItems(["demo"])
        dataSource.apply(snapshot, animatingDifferences: false)
    }
}

extension UIView {
    func pin(
        to object: CanBePinnedTo,
        top: CGFloat = 0,
        bottom: CGFloat = 0,
        leading: CGFloat = 0,
        trailing: CGFloat = 0
    ) {
        self.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            self.topAnchor.constraint(equalTo: object.topAnchor, constant: top),
            self.bottomAnchor.constraint(equalTo: object.bottomAnchor, constant: bottom),
            self.leadingAnchor.constraint(equalTo: object.leadingAnchor, constant: leading),
            self.trailingAnchor.constraint(equalTo: object.trailingAnchor, constant: trailing),
        ])
    }
    
    func pinToSuperview(
        top: CGFloat = 0,
        bottom: CGFloat = 0,
        leading: CGFloat = 0,
        trailing: CGFloat = 0,
        file: StaticString = #file,
        line: UInt = #line
    ) {
        guard let superview = self.superview else {
            print(">> \(#function) failed in file: \(String.localFilePath(from: file)), at line: \(line): could not find \(Self.self).superView.")
            return
        }
        
        self.pin(to: superview, top: top, bottom: bottom, leading: leading, trailing: trailing)
    }
    
    func pinToSuperview(constant c: CGFloat = 0, file: StaticString = #file, line: UInt = #line) {
        self.pinToSuperview(top: c, bottom: -c, leading: c, trailing: -c, file: file, line: line)
    }
}

@MainActor
protocol CanBePinnedTo {
    var topAnchor: NSLayoutYAxisAnchor { get }
    var bottomAnchor: NSLayoutYAxisAnchor { get }
    var leadingAnchor: NSLayoutXAxisAnchor { get }
    var trailingAnchor: NSLayoutXAxisAnchor { get }
}

extension UIView: CanBePinnedTo { }
extension UILayoutGuide: CanBePinnedTo { }

extension String {
    static func localFilePath(from fullFilePath: StaticString = #file) -> Self {
        URL(fileURLWithPath: "\(fullFilePath)").lastPathComponent
    }
}

Resize UICollectionViewListCell

Unfortunately, as soon as I insert a leading view in the cell:

let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in
    let contentView = cell.contentView
    let leadingView = UIView()
    leadingView.backgroundColor = .systemRed
    let textField = UITextField()
    textField.placeholder = "Placeholder"
    textField.font = .systemFont(ofSize: 100)
    
    contentView.addSubview(leadingView)
    contentView.addSubview(textField)
    
    leadingView.translatesAutoresizingMaskIntoConstraints = false
    textField.translatesAutoresizingMaskIntoConstraints = false
    
    NSLayoutConstraint.activate([
        leadingView.centerYAnchor.constraint(equalTo: contentView.layoutMarginsGuide.centerYAnchor),
        leadingView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
        leadingView.widthAnchor.constraint(equalTo: contentView.layoutMarginsGuide.heightAnchor),
        leadingView.heightAnchor.constraint(equalTo: contentView.layoutMarginsGuide.heightAnchor),
        
        textField.topAnchor.constraint(equalTo: contentView.topAnchor),
        textField.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
        textField.leadingAnchor.constraint(equalTo: leadingView.trailingAnchor, constant: 16),
        textField.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
        textField.heightAnchor.constraint(greaterThanOrEqualToConstant: 44)
    ])
}

the cell does not self-size, and in particular it does not accomodate the text field: Resize UICollectionViewListCell

What would be the best way to make the cell resize automatically?


Solution

  • Use UIListContentConfiguration:

    import UIKit
    
    class ViewController: UIViewController {
        var collectionView: UICollectionView!
        
        var dataSource: UICollectionViewDiffableDataSource<String, String>!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            configureHierarchy()
            configureDataSource()
        }
    
        func configureHierarchy() {
            collectionView = .init(frame: .zero, collectionViewLayout: createLayout())
            view.addSubview(collectionView)
            collectionView.frame = view.bounds
        }
        
        func createLayout() -> UICollectionViewLayout {
            UICollectionViewCompositionalLayout { section, layoutEnvironment in
                let config = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
                return NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment)
            }
        }
        
        func configureDataSource() {
            let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, itemIdentifier in
                var contentConfig = CustomListContentConfiguration()
                contentConfig.placeholder = "Placeholder"
                cell.contentConfiguration = contentConfig
            }
            
            dataSource = .init(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
                collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: itemIdentifier)
            }
            
            var snapshot = NSDiffableDataSourceSnapshot<String, String>()
            snapshot.appendSections(["main"])
            snapshot.appendItems(["demo"])
            dataSource.apply(snapshot, animatingDifferences: false)
        }
    }
    
    class CustomListContentConfiguration: UIContentConfiguration {
        var placeholder: String?
        
        func makeContentView() -> UIView & UIContentView {
            return CustomListContentView(configuration: self)
        }
        
        func updated(for state: UIConfigurationState) -> Self {
            // Not handling state changes in this example, so just return self
            return self
        }
    }
    
    class CustomListContentView: UIView, UIContentView {
        var configuration: UIContentConfiguration
        
        init(configuration: UIContentConfiguration) {
            self.configuration = configuration
            super.init(frame: .zero)
            configureSubviews()
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        
        func configureSubviews() {
            guard let config = configuration as? CustomListContentConfiguration else { return }
            
            let leadingView = UIView()
            leadingView.backgroundColor = .systemRed
            let textField = UITextField()
            textField.placeholder = config.placeholder
            textField.font = .systemFont(ofSize: 100)
            
            addSubview(leadingView)
            addSubview(textField)
            
            leadingView.translatesAutoresizingMaskIntoConstraints = false
            textField.translatesAutoresizingMaskIntoConstraints = false
            
            NSLayoutConstraint.activate([
                leadingView.centerYAnchor.constraint(equalTo: layoutMarginsGuide.centerYAnchor),
                leadingView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
                leadingView.widthAnchor.constraint(equalTo: layoutMarginsGuide.heightAnchor),
                leadingView.heightAnchor.constraint(equalTo: layoutMarginsGuide.heightAnchor),
                
                textField.topAnchor.constraint(equalTo: topAnchor),
                textField.bottomAnchor.constraint(equalTo: bottomAnchor),
                textField.leadingAnchor.constraint(equalTo: leadingView.trailingAnchor, constant: 16),
                textField.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
                textField.heightAnchor.constraint(greaterThanOrEqualToConstant: 44)
            ])
        }
    }
    

    Resize UICollectionViewListCell