iosuicollectionviewuikituicollectionviewlayoutuicollectionviewflowlayout

Is there any way to using UICollectionViewCell's self-sizing only in vertical direction?


I am writing a demo about the difference using compositional layout & traditional flow layout with UICollectionView.

I found that if I want to set the cell’s width equal to the UICollectionView’s width , but the cell’s height is determined by the cell’s content (self-sizing only in vertical).

  1. This can be easily with compositional layout

    let height = NSCollectionLayoutDimension.estimated(144)
    let itemSize = NSCollectionLayoutSize(widthDimension: NSCollectionLayoutDimension.fractionalWidth(1), heightDimension: height)
    
  2. But, if I use traditional flow layout, I can only set flow layout’s estimatedItemSize like this:

    layout.estimatedItemSize = CGSizeMake(collectionView.width, 140)
    

    But, after layout, the cell’s final width will always determined by the actual content, not the width equal to collectionView’s width.

Is there any best practice about this kind vertical-only self-sizing?


Solution

  • There are a variety of ways. One is to implement preferredLayoutAttributesFitting(_:), using required for width, but a low priority for height:

    class Cell: UICollectionViewCell {
        @IBOutlet weak var label: UILabel!
    
        override func preferredLayoutAttributesFitting(
            _ layoutAttributes: UICollectionViewLayoutAttributes
        ) -> UICollectionViewLayoutAttributes {
            let attributes = super.preferredLayoutAttributesFitting(layoutAttributes)
    
            let size = CGSize(width: superview!.bounds.width - 40, height: 40)
            attributes.frame.size = contentView.systemLayoutSizeFitting(
                size,
                withHorizontalFittingPriority: .required,
                verticalFittingPriority: .defaultLow
            )
            return attributes
        }
    }
    

    That yields:

    collection view cells with fixed width but dynamic height


    The full MRE:

    class Cell: UICollectionViewCell {
        @IBOutlet weak var label: UILabel!
    
        override func preferredLayoutAttributesFitting(
            _ layoutAttributes: UICollectionViewLayoutAttributes
        ) -> UICollectionViewLayoutAttributes {
            let attributes = super.preferredLayoutAttributesFitting(layoutAttributes)
    
            let size = CGSize(width: superview!.bounds.width - 40, height: 40)
            attributes.frame.size = contentView.systemLayoutSizeFitting(
                size,
                withHorizontalFittingPriority: .required,
                verticalFittingPriority: .defaultLow
            )
            return attributes
        }
    }
    
    class ViewController: UIViewController {
        @IBOutlet weak var collectionView: UICollectionView!
    
        // some strings of increasing lengths
    
        let strings = (0...15).map {
            let formatter = NumberFormatter()
            formatter.numberStyle = .spellOut
            var value = 0.0
            for i in 0 ... $0 {
                value += pow(10.0, Double(i))
            }
            return formatter.string(for: value)!
        }
    
        var dataSource: UICollectionViewDiffableDataSource<Section, String>!
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            dataSource = .init(collectionView: collectionView) { [weak self] collectionView, indexPath, itemIdentifier in
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
                cell.label.text = self?.strings[indexPath.item]
                return cell
            }
            collectionView.dataSource = dataSource
            applySnapshot()
        }
    
        func applySnapshot() {
            var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
            snapshot.appendSections([.main])
            snapshot.appendItems(strings)
            dataSource.apply(snapshot)
        }
    }
    
    extension ViewController {
        enum Section: Hashable {
            case main
        }
    }