Search code examples
swiftxcode-storyboard

UITableViewCell image not full width


I'm trying to fill a UITableView will images that stretch to the full width and automatically set their heights. I've tried setting estimatedHeightForRowAtIndexPath and heightForRowAt to UITableView.automaticDimension but they seem to show up really small.

My CustomTableViewCell has a UIImage with constraints set to 0 on all sides.

I'm starting to think it might be something to do with main.storyboard being a Viewcontroller containing a UITableView, the datasource and delegate of which are set to the view controller. Should the main.storyboard instead be a UITableViewController?

Can anyone tell me what I'm doing wrong?

This is how it currently looks:

simulator uitableview custom cells small images

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!

    private var imageUrls:[String] = ["https://via.placeholder.com/200","https://via.placeholder.com/400","https://via.placeholder.com/600"]

    override func viewDidLoad() {
        super.viewDidLoad()

        tableView.estimatedRowHeight = 320
        tableView.rowHeight = UITableView.automaticDimension

        let textFieldCell = UINib(nibName: "CustomTableViewCell",
                              bundle: nil)
        tableView.register(textFieldCell, forCellReuseIdentifier: "CustomTableViewCell")
    }


}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return imageUrls.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        if let cell = tableView.dequeueReusableCell(withIdentifier: "CustomTableViewCell") as? CustomTableViewCell {

            let imageUrl = imageUrls[indexPath.row]
            cell.messageImage.sd_setImage(with: URL(string: imageUrl), placeholderImage: nil , options: SDWebImageOptions(rawValue: 0))

            return cell
        }

        return UITableViewCell()
    }

    func tableView(_ tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: IndexPath) -> CGFloat {
        return 320
    }

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

EDIT: here is what it looks like when then image content mode is set to Aspect Fill.

aspect fill

I've also just noticed a warning:

[Warning] Warning once only: Detected a case where constraints ambiguously suggest a height of zero for a table view cell's content view. We're considering the collapse unintentional and using standard height instead.

Solution

  • The problem is that, there is no way you can update the height of a cell after it was created and drawn onto the screen. But what you can do is when you download an image, you notify the TableView to reload that specific row to which the image belongs to. This will call the heightForRowAtIndexPath, but you will know the exact height of the image now.

    Define an array where you will store the fetched images in your ViewController.

    private var images: [UIImage] = []
    

    Now when creating the cell you will either pass that image to the cell, or you will lunch a download task with a callback that will notify the TableView to reload a specific row.

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "YourCell") as? YourCell
    
        if images.count > indexPath.row {
            // We have the image, load it
            cell?.setupContentWith(image: images[indexPath.row])
        } else {
            // We do not have the image, download it
            cell?.setupContent(url: URL(string: imageUrls[indexPath.row]), completion: { [weak self] image in
                self?.images.append(image)
                tableView.reloadRows(at: [indexPath], with: .none)
            })
        }
        return cell ?? UITableViewCell()
    }
    

    The functions inside the TableViewCell could look something like this:

    private func setupContent(url: URL?, completion: @escaping (UIImage) -> Void) {
        guard let url = url else { return }
        yourImageView?.downloaded(from: url, completion: completion)
    }
    
    private func setupContentWith(image: UIImage) {
        yourImageView?.image = image
    }
    

    And finally when you don't have the image you can return some default height which you think is appropriate. And in case you have the image, you return it's height.

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        if images.count > indexPath.row {
            // We have the image, return it's height
            return images[indexPath.row].size.height
        } else {
            // We do not have the image, return some default height
            return 50 // Or some height you find approriate
        }
    }