简体   繁体   English

在UITableViewCell中使用包含带异步图像加载的UICollectionView的自动布局

[英]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. TL; DR :第一次显示包含集合视图的单元格时autolayout引擎计算的高度总是错误的。 Calling reloadData() on the table view fixes the issue but also makes the table view to jump and to be unusable. 在表视图上调用reloadData()可以解决问题,但也会使表视图跳转并无法使用。 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. 当表视图第一次尝试显示图库单元格时,我收到了Super Annoying Encapsulated-Height-Layout问题。 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. 我可以强制表视图正确显示单元格的唯一方法是在第一次加载图像时在表视图上调用reloadData ...这使得表视图跳转并基本上无法使用。

I'm using Kingfisher to retrieve the images, here's the code: 我正在使用Kingfisher来检索图像,这里是代码:

UICollectionViewCell Data Source: UICollectionViewCell数据源:

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) : 下面是在imageIsReadyForCellAtIndexPath(image: UIImage, collectionView: UICollectionView, screenshotIndex: Int)调用RooViewController上的委托时发生的情况:

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 : 以下是我设置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: 并持有它的Custom Cell

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:) 最后但并非最不重要的是,我在tableView(_:willDisplayCell:forRowAtIndexPath:)设置了集合视图的高度约束tableView(_:willDisplayCell:forRowAtIndexPath:)

I tried to to set it also in cellForRowAtIndexPath(_:) but it doesn't change anything. 我试图在cellForRowAtIndexPath(_:)设置它,但它不会改变任何东西。

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). 可能,(高度约束)修复此警告只是使该约束的优先级为999(如果它是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() 并删除tableView.reloadData()

Please give me a feedback after you try it. 试试后请给我一个反馈。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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