1

I have a horizontal UICollectionView in which I display images that are loaded asynchronously. The cells are supposed to have their width fit the image.

In viewDidLoad of my view controller, I set the estimated cell size:

(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.estimatedItemSize = CGSize(width: 400, height: 400)

In cellForItem, I start the download task using Kingfisher:

cell.imageView.kf.setImage(with: url) { (_, _, _, _) in 
    cell.layoutSubviews()
}

Inside my cell, I have the following code inside the layoutSubviews method:

// 326 is the image view's height
// .aspectRatio is a custom extension that returns: size.width / size.height
imageViewWidthConstraint.constant = 326 * image.aspectRatio
layoutIfNeeded()

In the storyboard, I have properly setup the layout constraints so that imageViewWidthConstraint is respected for the cell's width.

The following is the result when running my app:

wrong cell widths

As you can see, the cells have a width of 400, although the image was loaded and the layout updated. And, as result, the images are stretched to fill the image view.
When I scroll to the right & then back, the cells are removed from the collection view and loaded back in, and are now properly laid out:

right cell widths

While scrolling, the cells adjust their width, sometimes to the correct width, sometimes to the wrong one.

What am I doing wrong here?

LinusGeffarth
  • 27,197
  • 29
  • 120
  • 174
  • 1
    Hi, i just implemented same thing with tableView. Here is the [Link](https://github.com/AwaisFayyaz/Dynamic-height-TableView-with-dynamic-images). I had to keep width constant and have height dynamic based on image dimensions. Take a look at the code to get an idea. Basically you should be returning size in `SizeForItemAt IndexPath` and your image should be pinned to the collection view. – Awais Fayyaz Mar 20 '19 at 11:39
  • @AwaisFayyaz How would you do this for a collection view? – LinusGeffarth Mar 20 '19 at 12:23

1 Answers1

3

Since your images come in asynchronously it may take some time to be loaded. Only once they are loaded you can actually know the size or ratio of the image. That means for every image that is loaded you need to "reload" your layout. From a short search this looks promising.

At least this way you may get some animations, otherwise your cells will just keep jumping when images start to be loaded.

In any case I would advise you to avoid this. If I may assume; you are getting some data from server from which you use delivered URLs to download the images and show them on the collection view. The best approach (if possible) is to request that the API is extended so that you receive dimensions of images as well. So instead of

{
    id: 1,
    image: "https://..."
}

You could have

{
    id: 1,
    image: {
        url: "https://...",
        width: 100,
        height: 100
    } 
}

You can now use these values to generate aspect ratio width/height before you download the images.

Next to that I don't really see any good solution for the whole thing to look nice (without jumping around).

Matic Oblak
  • 16,318
  • 3
  • 24
  • 43
  • The images do take a sec to load but then the layout isn't updated... Thanks for the suggestions to pass width & height parameters in the API response, I'll probably go with that as it should yield the best visual results. – LinusGeffarth Mar 20 '19 at 12:15
  • I'll not accept your answer, though, because, technically, it doesn't answer my question Maybe someone comes along with a solution some day. Have my upvote as *thanks* instead. – LinusGeffarth Mar 20 '19 at 12:16
  • @LinusGeffarth "but then the layout isn't updated"; that part should be fixed by the link I provided. But the thing is you would need to place that code into some callback from the tool you are using that sets the new image on the image view... – Matic Oblak Mar 20 '19 at 12:17
  • Indeed, I previously had `reloadItems(at:)` for this which I called after each image finished downloading. Issue was that it jumped around when scrolling b/c new cells were loaded... – LinusGeffarth Mar 20 '19 at 12:18
  • @LinusGeffarth ah yes, you can use that same closure to call that "reload". It should animate your resize. But I would still not expect this to be a sufficient result. But technically it seems impossible to solve it without resizing unless you receive image sizes before the actual images. Well maybe you could call HEAD on all the images to get their sizes before you inject data into your view. It would be better than downloading all the images but would still require N requests to be made. – Matic Oblak Mar 20 '19 at 12:21
  • True. But, that approach wouldn't support pagination... Oh boy, ha. I guess passing the size params in the API response is the best solution. So I guess your answer *does* answer my question. – LinusGeffarth Mar 20 '19 at 12:22
  • 1
    @LinusGeffarth well it would also enable paging. Imagine you would create a request for a single page and get let's s 200 items all having a single image. At this point you would not yet call a completion block but rather create 200 parallel HEAD requests for each of the images. Once you get 200 sizes you would inject sizes into your payload and then call the completion. So basically you would preserve exactly the same system but wait for those extra data. – Matic Oblak Mar 20 '19 at 12:33