Search code examples
swiftuitableviewasynchronousuiimageview

Autolayout images inside cells in tableview. Correct layout but only once scrolling down and back up?


Im trying to get tableview cells with auto resizing images to work. Basically I want the image width in the cell to always be the same, and the height to change in accordance with the aspect ratio of the image.

I have created a cell class, which only has outlets for a label, imageView and a NSLayoutConstraint for the height of the image. I have some async methods to download an image and set it as the image for the cell imageView. Then the completion handle gets called and I run the following code to adjust the height constraint to the correct height:

cell.cellPhoto.loadImageFromURL(url: photos[indexPath.row].thumbnailURL, completion: {
            // Set imageView height to the width
            let imageSize = cell.cellPhoto.image?.size
            let maxHeight = ((self.tableView.frame.width-30.0)*imageSize!.height) / imageSize!.width
            cell.cellPhotoHeight.constant = maxHeight
            cell.layoutIfNeeded()
        })
        return cell

And here is the UIImageView extension I wrote which loads images:

func loadImageFromURL(url: String, completion: @escaping () -> Void) {
        let url = URL(string: url)
        makeDataRequest(url: url!, completion: { data in
            DispatchQueue.main.async {
                self.image = UIImage(data: data!)
                completion()
            }
        })
    }

And the makeDataRequest function which it calls:

func makeDataRequest(url: URL, completion: @escaping (Data?) -> Void) {
    let session = URLSession.shared

    let task = session.dataTask(with: url, completionHandler: { data, response, error in
        if error == nil {
            let response = response as? HTTPURLResponse
            switch response?.statusCode {
                case 200:
                    completion(data)
                case 404:
                    print("Invalid URL for request")
                default:
                    print("Something else went wrong in the data request")
            }
        } else {
            print(error?.localizedDescription ?? "Error")
        }
    })

    task.resume()
}

This works for all the cells out of frame, but the imageviews in the cells in the frame are small. Only when I scroll down and then back up again do they correctly size. How do I fix this? I know other people have had this issue but trying their fixes did nothing.


Solution

  • I had to sorta recreate the problem to understand what was going on. Basically you need to reload the tableview. I would do this when a picture finishes downloading.

    In the view controller that has the table view var. Add this to the viewDidLoad() function.

       override func viewDidLoad() {
            super.viewDidLoad()
            tableView.delegate = self
            tableView.dataSource = self
    
            //Create a notification so we can update the list from anywhere in the app. Good if you are calling this from an other class.
             NotificationCenter.default.addObserver(self, selector: #selector(loadList), name: NSNotification.Name(rawValue: "loadList"), object: nil)
    
        }
    
       //This function updates the cells in the table view
       @objc func loadList(){
              //load data here
              self.tableView.reloadData()
          }
    

    Now, when the photo is done downloading, you can notify the viewcontroller to reload the table view by using the following,

    func loadImageFromURL(url: String, completion: @escaping () -> Void) {
            let url = URL(string: url)
            makeDataRequest(url: url!, completion: { data in
                DispatchQueue.main.async {
                    self.image = UIImage(data: data!)
                    completion()
                    //This isn't the best way to do this as, if you have 25+ pictures,
                    //the list will pretty much freeze up every time the list has to be reloaded.
                    //What you could do is have a flag to check if the first 'n' number of cells 
                    //have been loaded, and if so then don't reload the tableview.
    
                    //Basically what I'm saying is, if the cells are off the screen who cares.
                    NotificationCenter.default.post(name: NSNotification.Name(rawValue: "loadList"), object: nil)
                }
            })
        }
    

    Heres something I did to have better Async, see below. Sim picture

    My code as follows, I didn't do the resizing ratio thing like you did but the same idea applies. It's how you go about reloading the table view. Also, I personally don't like writing my own download code, with status code and everything. It isn't fun, why reinvent the wheel when someone else has done it?

    Podfile

    pod 'SDWebImage',  '~> 5.0' 
    

    mCell.swift

    class mCell: UITableViewCell {
        //This keeps track to see if the cell has been already resized. This is only needed once.
        var flag = false
        @IBOutlet weak var cellLabel: UILabel!
        @IBOutlet weak var cell_IV: UIImageView!
    
        override func awakeFromNib() { super.awakeFromNib() }
    }
    

    viewController.swift (Click to see full code) I'm just going to give the highlights of the code here.

    //Set the image based on a url
    //Remember this is all done with Async...In the backgorund, on a custom thread.
    mCell.cell_IV.sd_setImage(with: URL(string: ViewController.cell_pic_url[row])) { (image, error, cache, urls) in
        // If failed to load image
        if (error != nil) {
            //Set to defult
             mCell.cell_IV.image = UIImage(named: "redx.png")
         }
        //Else we got the image from the web.
        else {
             //Set the cell image to the one we downloaded
             mCell.cell_IV.image = image
    
             //This is a flag to reload the tableview once the image is done downloading. I set a var in the cell class, this is to make sure the this is ONLY CALLED once. Otherwise the app will get stuck in an infinite loop.
             if (mCell.flag != true){
                 DispatchQueue.main.asyncAfter(deadline: .now() + 0.025){ //Nothing wrong with a little lag.
                          NotificationCenter.default.post(name: NSNotification.Name(rawValue: "loadList"), object: nil)
                          mCell.flag = true
                       }
             }
    
    
    
        }
    }