Jack Jack - 1 month ago 16
Swift Question

Images not loading in uitableview cell until I scroll

I have done a lot of research and know there are a lot of posts on this topic, But none have seemed to help.

I am downloading image url's and other stuff into an array from my server and putting everything into a posts = AnyObject array.

So far that is great!

Now In my UITableViewCell I access that array(posts) and extract the url for the images and then add that url to a image cache I have. I shall show some code to make it more comprehensible!

This is the when I cache the image in a "Extension.swift" file :

extension UIImageView {

func loadImageUsingCacheWithUrlString(urlString: String) {

self.image = nil
//check cache for image first
if let cachedImage = imageCache.object(forKey: urlString as AnyObject) as? UIImage {

self.image = cachedImage
return
}
//otherwise fire off a new download
let url = NSURL(string: urlString)
URLSession.shared.dataTask(with: url! as URL, completionHandler: { (data, response, error) in

//download hit an error so lets return out
if error != nil {
print(error)
return
}

DispatchQueue.main.async(execute: { () -> Void in
if let downloadedImage = UIImage(data: data!) {

// let width = CGSize(width: (widthOfView / 3) - 1, height: (widthOfView / 3) - 1)
let width = CGSize(width: widthOfView, height: downloadedImage.size.height * (widthOfView / downloadedImage.size.width))
//print(imageWithImage(downloadedImage, scaledToSize: width))
imageCache.setObject(imageWithImage(downloadedImage, scaledToSize: width), forKey: urlString as AnyObject)
self.image = imageWithImage(downloadedImage, scaledToSize: width)
}
})

}).resume()


}
}

now I access that from my tableView Cell and attach the url's so the images start downloading like so:

let post = posts[(indexPath as NSIndexPath).section]
DispatchQueue.main.async(execute: {() -> Void in
//cell.postImage.image = UIImage(named: "ImagePlaceHolder")
let path = post["path"] as? String
if let postsUrl = path {

cell.postImage.loadImageUsingCacheWithUrlString(urlString: postsUrl)

}
})


All works great!

But here is the problem..:

the TableView loads with the rest of the stuff from the Posts array but the images have not been added!

So because the images have not added the cell size is incorrect and too small, but if I scroll down and back up everything loads as it should!

Now I have read many other questions which say add "self.tableView.reloadData()", yes that works but then everytime a cell appears the tableview gets reload and you can see it on the screen, it looks terrible!

Another thing I found which I have tried is to add "cell.layoutSubviews()" that also works nicely and I prefer it too, BUT the first cell that you see at the begining remain small without an image even the the ones that are not on the screen have loaded correctly!

So to get those couple which are displayed first I have to scroll down and back up again!

Question..?! I would like to be able to load the tableView without any of those problems! So when I launch my app everything loads correctly without having to scroll up or down!

I know adding height/width to the UIImageView would work, but as the images can be different sizes I would prefer not to do that!!

If anyone can help in anyway or put me in the right direction it is much appreciated!!

Regards.

Answer

The problem is, that in the moment you populate your Cells, the Image simply doesn't exist yet. You need to download the whole desired data and call tableView.reloadData() at the end of the download. Not for every cell, but after downloading all the desired pictures. That way the tableView will not lag when you scroll.

First of all: I recommend using Structs/Models instead of AnyObject Arrays.

ContentStruct.swift
import Foundation

struct ContentStruct {

    var picture: String = ""
}

What you should do is something like this:

var cellContentArray: [ContentStruct] = []

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

      let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! Cell

      let cellContent = cellContentArray[indexPath.row]
      cell.bindData(cellContent)          // this is your function to bind the data into the cell

      return cell
}

class Cell: UITableViewCell {

      func bindData(content: ContentStruct) {

            if content.picture == "" {
                 pictureImageView.hidden = true
            } else {
                 pictureImageView.hidden = false
            }

            let imgurl = NSURL(string: content.picture)
            pictureImageView.sd_setImageWithURL(imgurl)
      }
}

I'm always using SDWebImage. It handles everything for you. All you need to do is to import and use it like I just provided.

import SDWebImage

Then. To download your pictures (as links for example from Firebase, use following:

override func viewDidAppear(animated: Bool) {

    loadContent()
}

// in this hypothetical function we're downloading links to images as Strings from 
// our Firebase backend and we're saving them as ContentStruct Models

func loadContent() {

    let usersRef = firebase.child("Content")
    usersRef.observeEventType(.Value, withBlock: { snapshot in

        if snapshot.exists() {

            self.cellContentArray.removeAll()

            let sorted = (snapshot.value!.allValues as NSArray).sortedArrayUsingDescriptors([NSSortDescriptor(key: "date",ascending: false)])      // sorts your content by date (if you have set date as part of the Firebase array)

            for element in sorted {

                let pic = element.valueForKey("picture")! as? String

                let t = ContentStruct(picture: pic!)

                self.cellContentArray.append(t)                    
            }
        }
        self.tableView.reloadData() 
    })   
}

Like this your content will be loaded and as soon as the download is completed (usually but depending on the connection speed) while the Screen loads, the UITableView will be reloaded and populated with your desired data including your images.

PS: If you provide me where you get your pictures from, I can change the code matching your needs. This is just a "general" idea of how to get the "links" of pictures as a String from Firebase.

To your thumbnail question asked in the comments:

So what I am doing is pre saving the imageLinks into my Firebase. So I have Struct Model with a thumbnail Image that is way smaller and with way lower quality and an full-size original Image.

ContentStruct.swift
import Foundation

struct ContentStruct {

    var thumbnail: String = ""
    var originalImage: String = ""
}

Then I'm downloading just as before, but I download both links. Thumbnail and originalImage.

func loadContent() {

    let usersRef = firebase.child("Content")
    usersRef.observeEventType(.Value, withBlock: { snapshot in

        if snapshot.exists() {

            self.cellContentArray.removeAll()

            let sorted = (snapshot.value!.allValues as NSArray).sortedArrayUsingDescriptors([NSSortDescriptor(key: "date",ascending: false)])      // sorts your content by date (if you have set date as part of the Firebase array)

            for element in sorted {

                let thumb = element.valueForKey("thumbnail")! as? String
                let orig = element.valueForKey("originalImage")! as? String

                let t = ContentStruct(thumbnail: thumb!, originalImage: orig!)

                self.cellContentArray.append(t)                    
            }
        }
        self.tableView.reloadData() 
    })   
}

Then just as provided above, I populate the tableView, but instead of using the image as before, I populate it with the thumbnail.

      func bindData(content: ContentStruct) {

            if content.picture == "" {
                 pictureImageView.hidden = true
            } else {
                 pictureImageView.hidden = false
            }

            let imgurl = NSURL(string: content.thumbnail)
            pictureImageView.sd_setImageWithURL(imgurl)
      }

Then I'm using the IDMPhotoBrowser as a Photo Browser. It is set up like this:

import IDMPhotoBrowser

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

     let content = cellContentArray[indexPath.row]
     let imageLink = content.originalImage
     openImageWithIDMPhoto(imageLink, vc: self)
}

And I have created this reusable helper function to open Images with the IDMPhotoBrowser

func openImageWithIDMPhoto(imageLink: String, vc: UIViewController) {

    let fileUrl = NSURL(string: imageLink)

    getDataFromUrl(fileUrl!) { (data, response, error)  in
        guard let data = data where error == nil else { return }
        dispatch_async(dispatch_get_main_queue()) {

            let image = UIImage(data: data)

            let photos = IDMPhoto.photosWithImages([image!])
            let browser = IDMPhotoBrowser(photos: photos)

            vc.presentViewController(browser, animated: true, completion: {
                //
            })
        }
    }
}

If you want to have a blurry effect:

var blurEffect = UIBlurEffect(style: UIBlurEffectStyle.Dark)
var blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = myThumbnailImageView.bounds
blurEffectView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight] // for supporting device rotation
myThumbnailImageView.addSubview(blurEffectView) // put it on your imageView

Result:

enter image description hereenter image description here

Scale UIImage Extension:

extension UIImage {

    func scale(toSize newSize:CGSize) -> UIImage {

        // make sure the new size has the correct aspect ratio
        let aspectFill = self.size.resizeFill(newSize)

        UIGraphicsBeginImageContextWithOptions(aspectFill, false, 0.0);
        self.drawInRect(CGRectMake(0, 0, aspectFill.width, aspectFill.height))
        let newImage:UIImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()

        return newImage
    }
}
Comments