How to update tableview cell height after image downloaded and height constraint changed swift?

Viewed 8349

How to update tableview cell height after updating image height constraint of image downloaded async?

How to trigger tableView cell relayout after image downloaded and constraints changed?

What's the best method to do this?

Already tried putting the code inside Dispatch main queue, but same bad results. I'm doing this in cellForRow method, also moved it to willDisplayCell. Again and again this problem...

Example of code using Kingfisher library for image caching:

    if let imgLink = post.imageLink {
                if let url = URL(string: imgLink) {

                    cell.postImage.kf.setImage(with: url, placeholder: UIImage(), options: nil, progressBlock: nil) { (image, error, cacheType, imageURL) in

                        if let image = image, cell.tag == indexPath.row {
                            cell.heightConstraint.constant = image.size.height * cell.frame.size.width / image.size.width   
                        }
                    }
                }
    }
4 Answers

IMHO, As ppalancica pointed out calling beginUpdates and endUpdates is the ideal way. You can't refer tableView from inside UITableViewCell and the proper way is to use a delegate and call beginUpdates and endUpdates from ViewController implementing delegate.

Delegate:

protocol ImageCellDelegate {
    func onLayoutChangeNeeded()
}

UITableViewCell implementation:

class ImageCell: UITableViewCell {
    var imageView: UIImageView = ...
    var delegate: ImageCellDelegate?
    ...

    func setImage(image: UIImage) {
        imageView.image = image
        //calling delegate implemented in 'ViewController'
        delegate?.onLayoutChangeNeeded()
    }

    ...
}

ViewController Implementation:

class ViewController: UIViewController, UITableViewDataSource, ImageCellDelegate {
    var tableView: UITableView = ...
    .....
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let imageCell = tableView.dequeueReusableCell(withIdentifier: id, for: indexPath) as! ImageCell
        //setting 'delegate' here
        imageCell.delegate = self
        return imageCell
    }
    
    //called from 'ImageCell' when 'image' is set inside 'setImage'
    func onLayoutChangeNeeded() {
        tableView.beginUpdates()
        tableView.endUpdates()
    }

    .....
}

I had the same problem. What you need to remember is Tableview reuse the cell and you are loading image async.

Recommended: You can do is to request your backhand team to provide you height and width of image so you can calculate cell height and return asap.

If you can't do that you can keep size of dowloaded image in your datasource. so before you download image check your datasource for size of image and update height constraint constant.

Another thing is you should do it in both cellForRow and willDisplay cell (I know it is not good practice but to satisfy tableview automatic dimension)

after update height constant you should use this pair of code to reload your cell.

    cell.setNeedsLayout()
    cell.layoutIfNeeded()

How I did

 if let imagesize = post.imageSize { 
      cell.updateHeightPerRatio(with: imagesize)

 } else {
      // Here load size from URL  and update your datasource 

  } 
  // Load image from URL with any SDWebimage or any other library you used

What I actually did and worked somehow is the following:

if (self.firstLoaded[indexPath.row] == false) {
                            self.firstLoaded[indexPath.row] = true
                            DispatchQueue.main.async {
                                self.tableViewController.tableView.reloadData()
                            }
}

firstLoaded just tells the table that this row has already received image from URL and calculated / stored correct height.

Also, I used this:

override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        cellHeights[indexPath] = cell.frame.size.height
}

override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return UITableViewAutomaticDimension
}

override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        if let height = cellHeights[indexPath] {
            return height
        }
        return 1500
}

I know that calling reloadData() is not a good practice, but it solved my problem. If anybody has some other advices, please do not hesitate to share it.

Thanks!

Related