1

I'm trying to use Compositional Layouts with UICollectionView where each cell contains an image with a dynamic aspect ratio. I'm using AutoLayout for the cells but I haven't been able to get the cells to auto-size correctly.

What's the right way to specify the cell's layout/constraints so that they have the correct height based on image aspect ratios?

In the code below, the images themselves get sized correctly, but their containing cells don't have the correct height (they just use their estimated height and don't adjust dynamically to the contained image). See screenshot.

(I know this is relatively straightforward using UICollectionViewFlowLayout, but I'm trying to learn compositional layouts and haven't been able to figure it out using this method)

Here's what I'm setting up in code:

For simplicity's sake of this post, I'm using a basic compositional layout with 1 group and 1 item using heightDimension: .estimated(300) as placeholder

let layout = UICollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in

            let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(300)))
            
            let group = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(300)), subitem: item, count: 1)
            
            let section = NSCollectionLayoutSection(group: group)
            
            return section
}
collectionView.setCollectionViewLayout(layout, animated: false)

Here's my code for the cell:


// during cellForItemAt, I call 
// cell.setImage(url: [url from API], size: [dynamic value from API])


class ImageCell: UICollectionViewCell {
    
    static let ImageCellIdentifier = String(describing: ImageCell.self)
    
    var aspectRatioConstraint: NSLayoutConstraint?
    
    private let imageView: UIImageView = {
        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        imageView.contentMode = .scaleAspectFill
        return imageView
    }()
    
    func setImage(url: URL, size: CGSize) {
        
        let aspectRatio = size.width / size.height
        
        if let aspectRatioConstraint = aspectRatioConstraint {
            imageView.removeConstraint(aspectRatioConstraint)
        }
        
        // set the image height constraint based on its aspect ratio
        // anchor it to contentView.widthAnchor
        aspectRatioConstraint = imageView.heightAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: aspectRatio)
        
        NSLayoutConstraint.activate([
            aspectRatioConstraint!,
        ])
        
        imageView.sd_setImage(with: url)
        
        contentView.layoutIfNeeded()

    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        contentView.addSubview(imageView)
        
        NSLayoutConstraint.activate([
            imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
            imageView.widthAnchor.constraint(equalTo: contentView.widthAnchor),
        ])
        
    }
}

enter image description here

4
  • Very easy to do this with a table view... would that be an option for your case?
    – DonMag
    Commented Feb 4, 2023 at 16:12
  • @DonMag certainly! even using a traditional UICollectionViewFlowLayout would do the trick ... but I'm trying to learn compositional layouts and given that they're being touted as the modern way of doing layouts, I'd like to see if I can achieve this (relatively common?) use case. A more complex version of this problem would be creating a masonry layout (think Pinterest iOS homefeed) with 2 columns of images that have dynamic heights. Again, straightforward for UICollectionViewFlowLayout -- and hopefully for Composition Layouts too. Commented Feb 4, 2023 at 17:02
  • 1
    Are you intending to do "one image per row"? if so... Your post says you are calling cell.setImage(url: [url from API], size: [dynamic value from API]) from cellForItemAt -- assuming that means you are pulling your url and size before layout begins, then you would want to use that data inside your compositional layout setup (instead of estimated heights).
    – DonMag
    Commented Feb 4, 2023 at 18:06
  • @DonMag thank you so much! that made it click for me - and yes, it works! I'll post my code here shortly in case it's helpful for others Commented Feb 5, 2023 at 17:08

1 Answer 1

1

Thanks to @DonMag in the comments, the answer is creating 1 section per item and specify the layout for each item-section accordingly:

Assuming images gets populated with a dynamic list of images (e.g. from an API)

func numberOfSections(in collectionView: UICollectionView) -> Int {
  return images.count
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  return 1 // return more if you want to render additional kinds of cell for each item besides the image
}

Setting up the compositional layout now looks like this -- the important part is ... heightDimension: .fractionalWidth(aspectRatio)


let layout = UICollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
    
    // use sectionIndex as an index into the list retrieved from API
        
    let image = images[sectionIndex]
    
    let aspectRatio = image.width / image.height
    
    let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(aspectRatio)))
    
    let group = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(aspectRatio)), subitem: item, count: 1)
    
    let section = NSCollectionLayoutSection(group: group)
    
    return section
}

collectionView.setCollectionViewLayout(layout, animated: false)

Thanks @DonMag!

1
  • I'm learning compositional layout too, so I came across this scenario too: I want to customize every item, and the api seems intent to set every item in a group the same size(or fixed number item sizes which you can't decide when you're not touch the datas). your solution take the trick(each section contains one group, which contains one item), it's far inconvenience than the simply old FlowLayout way, I wonder is there an official way to archive this.
    – walker
    Commented Jun 19, 2023 at 2:58

Not the answer you're looking for? Browse other questions tagged or ask your own question.