简体   繁体   中英

Using Auto Layout in UITableViewCell containing a UICollectionView with asynchronous image load

TL;DR : The height calculated by the autolayout engine when displaying the cell containing the collection view is always wrong the first time. Calling reloadData() on the table view fixes the issue but also makes the table view to jump and to be unusable. Any idea?

Explanations : I have a table view with many different cells of different heights.

One of these cells is an image gallery that can contain one or more images loaded asynchronously.

The constraints I have are the following:

  • if the gallery only contains one image and if this image is oriented in landscape, the height of the gallery should be the height of the image
  • else, the gallery has a fixed height.

The problems I face are the following:

  • I get the Super Annoying Encapsulated-Height-Layout issue thing when the table view tries to display the gallery cell the first time. This encapsulated height has always the wrong value, even though the height constraint on the collection view has been updated.

  • The table view consistently never gets the cell's size right at first try.

    • Even if the image is already retrieved when the cell is displayed, the cell displays poorly and I have to scroll up / down to hide it, then display it again to get the right size... until next time the cell size has to be computed again. See below: 在此输入图像描述
  • The only way I can get to force the table view to display the cell correctly is when I call reloadData on the table view once the image is loaded the first time... which makes the table view to jump and to be basically unusable.

I'm using Kingfisher to retrieve the images, here's 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
}

Here is what happens when the delegate gets called on the 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 set 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 Custom 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 the height constraint of the collection view in tableView(_:willDisplayCell:forRowAtIndexPath:)

I tried to to set it also in cellForRowAtIndexPath(_:) but it doesn't change anything.

Sorry for the massive code chunk, but this is driving me insane.

When That warning appears. it tell's you that it had to break one of your constraints in order to fix it. probabily, (the height constraint) to fix this warning just make that constraint's priority 999 (if it was 1000).


For updating the cell after setting the image to it. would you try this:

dispatch_async(dispatch_get_main_queue()) { () -> Void in
                            self.delegate?.imageIsReadyForCellAtIndexPath(image, collectionView: collectionView, screenshotIndex: indexPath.row)
                            cell.imageView.image = image
                            // get the content offset.
                             let offset = self.tableView.contentOffset
                             let visibleRect = CGRect(origin: offset, size: CGSize(width: 1, height: 1)
                            // reload the cell
                            tableView.beginUpdates()
                            tableView.endUpdates()
                            // to prevent the jumping you may scroll to the same visible rect back without animation.
                            self.tableView.scrollRectToVisible(visibleRect, animated: false)
                        }

and remove tableView.reloadData()

Please give me a feedback after you try it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM