TL DR . The height calculated by the autorun mechanism when displaying a cell containing a collection view is always erroneous for the first time. Calling reloadData() in the table view fixes the problem, but also makes table browsing abrupt and unusable. Any idea?
Explanations : I have a tabular view with many different cells of different heights.
One of these cells is an image gallery, which can contain one or more images loaded asynchronously.
I have the following limitations:
- If the gallery contains only one image, and if this image is oriented in landscape orientation, the height of the gallery should be the height of the image.
- else, the gallery has a fixed height.
The issues I am facing are as follows:
I get a Super Annoying Encapsulated-Height-Layout error message when a table view tries to display a gallery cell for the first time. This encapsulated height always has the wrong value, even if the height limit in the collection view is updated.
At first, the table never gets the cell size on the first try.
- Even if the image has already been extracted when the cell is displayed, the cell is displayed poorly, and I need to scroll up / down to hide it and then show it again to get the right size ... until next time, the cell size needs to be calculated again. See below:

- The only way I can get the table view to display the cell correctly is when I call reloadData on the table view as soon as the image loads for the first time ... which causes the table view to jump and be mostly unusable.
I use Kingfisher to extract images, here is the code:
UICollectionViewCell data source:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCellWithReuseIdentifier("CarouselCollectionViewCell", forIndexPath: indexPath) as! CarouselCollectionViewCell guard let collectionView = collectionView as? CarouselCollectionView else { return cell } if let imagesURLs = data.imagesURLs { let url = imagesURLs[indexPath.item] if let smallURL = url.small { KingfisherManager.sharedManager.retrieveImageWithURL( smallURL, optionsInfo: KingfisherOptionsInfo(), progressBlock: nil, completionHandler: { (image, error, cacheType, imageURL) -> () in if let image = image { self.delegate?.imageIsReadyForCellAtIndexPath(image, collectionView: collectionView, screenshotIndex: indexPath.row) cell.imageView.image = image } }) } } return cell }
This is what happens when a delegate receives a call to RooViewController in imageIsReadyForCellAtIndexPath(image: UIImage, collectionView: UICollectionView, screenshotIndex: Int) :
func imageIsReadyForCellAtIndexPath(image: UIImage, collectionView: UICollectionView, screenshotIndex: Int) { guard let collectionView = collectionView as? CarouselCollectionView else { return } guard let collectionViewIndexPath = collectionView.indexPath else { return } guard let screenshotsCount = feed?.articles?[collectionViewIndexPath.section].content?[collectionViewIndexPath.row].data?.imagesURLs?.count else { return } let key = self.cachedSizesIndexPath(collectionViewIndexPath: collectionViewIndexPath, cellIndexPath: NSIndexPath(forItem: screenshotIndex, inSection: 0)) var sizeToCache: CGSize! if screenshotsCount == 1 { // Resize the collectionView to fit a landscape image: if image.isOrientedInLandscape { sizeToCache = image.scaleToMaxWidthAndMaxHeight(maxWidth: Constants.maxImageWidth, maxHeight: Constants.maxImageHeight) } else { sizeToCache = image.scaleToHeight(Constants.maxImageHeight) } if collectionViewCellsCachedSizesObject.dict[key] == nil { let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout let imageWidth = sizeToCache.width let sidesInset = (collectionView.frame.width - imageWidth) / 2 print("sidesInset: ", sidesInset) flowLayout.sectionInset = UIEdgeInsets(top: 0, left: sidesInset, bottom: 0, right: sidesInset) collectionViewCellsCachedSizesObject.dict[key] = sizeToCache collectionView.heightConstraint.constant = sizeToCache.height collectionView.collectionViewLayout.invalidateLayout() collectionView.setNeedsUpdateConstraints() tableView.reloadData() } } else { let sizeToCache = image.scaleToHeight(Constants.maxImageHeight) if collectionViewCellsCachedSizesObject.dict[key] == nil { // && collectionViewCellsCachedSizesObject.dict[key] != sizeToCache { collectionViewCellsCachedSizesObject.dict[key] = sizeToCache collectionView.collectionViewLayout.invalidateLayout() } } }
Here is how I installed my Collection View :
class CarouselElement: Element { let collectionView: CarouselCollectionView func cachedSizesIndexPath(collectionViewIndexPath aCollectionViewIndexPath: NSIndexPath, cellIndexPath aCellIndexPath: NSIndexPath) -> String { return "\(aCollectionViewIndexPath), \(aCellIndexPath)" } override init(style: UITableViewCellStyle, reuseIdentifier: String?) { let layout = UICollectionViewFlowLayout() layout.sectionInset = UIEdgeInsets(top: 0, left: Constants.horizontalPadding, bottom: 0, right: Constants.horizontalPadding) layout.scrollDirection = .Horizontal collectionView = CarouselCollectionView(frame: CGRectZero, collectionViewLayout: layout) collectionView.translatesAutoresizingMaskIntoConstraints = false collectionView.registerClass(CarouselCollectionViewCell.self, forCellWithReuseIdentifier: "CarouselCollectionViewCell") collectionView.allowsMultipleSelection = false collectionView.allowsSelection = true collectionView.backgroundColor = Constants.backgroundColor collectionView.showsHorizontalScrollIndicator = false super.init(style: style, reuseIdentifier: reuseIdentifier) addSubview(collectionView) addConstraints(NSLayoutConstraint.constraintsWithVisualFormat( "|[collectionView]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["collectionView":collectionView])) addConstraints(NSLayoutConstraint.constraintsWithVisualFormat( "V:|[collectionView]-verticalPadding-|", options: NSLayoutFormatOptions(), metrics: ["verticalPadding":Constants.verticalPadding], views: ["collectionView":collectionView])) collectionView.heightConstraint = NSLayoutConstraint( item: collectionView, attribute: .Height, relatedBy: .Equal, toItem: nil, attribute: .NotAnAttribute, multiplier: 1.0, constant: 200) addConstraint(collectionView.heightConstraint) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setCarouselDataSourceDelegate(dataSourceDelegate: CarouselDataSourceDelegate?, indexPath: NSIndexPath, cachedHeight: CGFloat?) { collectionView.indexPath = indexPath if let height = cachedHeight { collectionView.heightConstraint.constant = height } collectionView.dataSource = dataSourceDelegate collectionView.delegate = dataSourceDelegate collectionView.reloadData() } override func prepareForReuse() { super.prepareForReuse() collectionView.contentOffset = CGPointZero }}
And the User cell holding it:
class CarouselCollectionViewCell: UICollectionViewCell { let imageView: UIImageView override init(frame: CGRect) { imageView = UIImageView.autolayoutView() as! UIImageView imageView.image = Constants.placeholderImage imageView.contentMode = .ScaleAspectFit super.init(frame: frame) translatesAutoresizingMaskIntoConstraints = false addSubview(imageView) addConstraints( NSLayoutConstraint.constraintsWithVisualFormat( "|[imageView]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["imageView":imageView])) addConstraints( NSLayoutConstraint.constraintsWithVisualFormat( "V:|[imageView]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["imageView":imageView])) } override func prepareForReuse() { imageView.image = Constants.placeholderImage } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
Last but not least, I set a height limit on the collection view in tableView(_:willDisplayCell:forRowAtIndexPath:)
I tried to set it also in cellForRowAtIndexPath(_:) , but didn't change anything.
Sorry for the massive piece of code, but it drives me crazy.