2

I am very new to swift and need some help with fetching images from URLs and storing them into a dictionary to reference into a UITableView. I've checked out the various threads, but can't find a scenario which meets by specific need.

I currently have the names of products in a dictionary as a key with the image URLs linked to each name:

let productLibrary = ["Product name 1":"http://www.website.com/image1.jpg", 
"Product name 2":"http://www.website.com/image2.jpg"]

I would need to get the actual images into a dictionary with the same product name as a key to add to the UITableView.

I currently have the images loading directly in the tableView cellForRowAt function, using the following code, but this makes the table view unresponsive due to it loading the images each time the TableView refreshes:

cell.imageView?.image = UIImage(data: try! Data(contentsOf: URL(string:
productLibrary[mainPlaces[indexPath.row]]!)!))

mainPlaces is an array of a selection of the products listed in the productLibrary dictionary. Loading the images initially up-front in a dictionary would surely decrease load time and make the UITableView as responsive as I need it to be.

Any assistance would be greatly appreciated!

@Samarth, I have implemented your code as suggested below (just copied the extension straight into the root of the ViewController.swift file above class ViewController.

The rest, I have pasted below the class ViewController class as below, but it's still not actually displaying the images in the tableview.

I've tried to do exactly as you've advised, but perhaps I'm missing something obvious. Sorry for the many responses but I just can't seem to get it working. Please see my exact code below:

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

    let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "Cell")

    cell.textLabel?.text = mainPlaces[indexPath.row]

    downloadImage(url: URL(string: productLibrary[mainPlaces[indexPath.row]]!)!)

    cell.imageView?.downloadedFrom(link: productLibrary[mainPlaces[indexPath.row]]!)

    return cell

}

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    performSegue(withIdentifier: "ProductSelect", sender: nil)

        globalURL = url[mainPlaces[indexPath.row]]!

}

func getDataFromUrl(url: URL, completion: @escaping (_ data: Data?, _  response: URLResponse?, _ error: Error?) -> Void) {
    URLSession.shared.dataTask(with: url) {
        (data, response, error) in
        completion(data, response, error)
        }.resume()
}

func downloadImage(url: URL) {
    print("Download Started")
    getDataFromUrl(url: url) { (data, response, error)  in
        guard let data = data, error == nil else { return }
        print(response?.suggestedFilename ?? url.lastPathComponent)
        print("Download Finished")
        DispatchQueue.main.async() { () -> Void in

            // self.imageView.image = UIImage(data: data)
            /* If you want to load the image in a table view cell then you have to define the table view cell over here and then set the image on that cell */
            // Define you table view cell over here and then write

            let cell = UITableViewCell(style: UITableViewCellStyle.default, reuseIdentifier: "Cell")

            cell.imageView?.image = UIImage(data: data)

        }
    }
}
  • do this : `performUIUpdatesOnMain { cell.imageView?.image = UIImage(data: try! Data(contentsOf: URL(string: productLibrary[mainPlaces[indexPath.row]]!)!)) }` – Samarth Kejriwal Jul 19 '17 at 15:34
  • Possible duplicate of [how to implement lazy loading of images in table view using swift](https://stackoverflow.com/questions/28694645/how-to-implement-lazy-loading-of-images-in-table-view-using-swift) – junaidsidhu Jul 19 '17 at 15:55
  • http://jamesonquave.com/blog/developing-ios-apps-using-swift-part-5-async-image-loading-and-caching/ – junaidsidhu Jul 19 '17 at 16:04
  • Hi All. Thanks for the replies. @Samarth, I tried to copy your code into my UITableView method, but I get an error message "use of unresolved identifier 'performUIUpdatesOnMain'. Not sure if there's a prior code / function I needed to implement, or if I'm putting this in the wrong place. Also, junaidsidhu, the post referring to the prior question gives me all sorts of errors when I copy this into swift and try to change the required variables. It seems to have been written for a prior version of swift? Sorry if I'm not understanding this, as mentioned I'm extremely new at this. – GarrethTrent Jul 19 '17 at 19:10

2 Answers2

4

You can load the images synchronously or asynchronously in your project.

Synchronous: means that your data is being loaded on the main thread, so till the time your data is being loaded, your main thread (UI Thread) will be blocked. This is what is happening in your project

Asynchronous: means your data is being loaded on a different thread other than UI thread, so that UI is not being blocked and your data loading is done in the background.

Try this example to load the image asynchronously :

Asynchronously:

Create a method with a completion handler to get the image data from your url

func getDataFromUrl(url: URL, completion: @escaping (_ data: Data?, _  response: URLResponse?, _ error: Error?) -> Void) {
    URLSession.shared.dataTask(with: url) {
        (data, response, error) in
        completion(data, response, error)
    }.resume()
}

Create a method to download the image (start the task)

func downloadImage(url: URL) {
    print("Download Started")
    getDataFromUrl(url: url) { (data, response, error)  in
        guard let data = data, error == nil else { return }
        print(response?.suggestedFilename ?? url.lastPathComponent)
        print("Download Finished")
        DispatchQueue.main.async() { () -> Void in

           // self.imageView.image = UIImage(data: data)
   /* If you want to load the image in a table view cell then you have to define the table view cell over here and then set the image on that cell */
           // Define you table view cell over here and then write 
           //      cell.imageView?.image = UIImage(data: data)

        }
    }
}

Usage:

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    print("Begin of code")
    if let checkedUrl = URL(string: "your image url") {
        imageView.contentMode = .scaleAspectFit
        downloadImage(url: checkedUrl)
    }
    print("End of code. The image will continue downloading in the background and it will be loaded when it ends.")
}

Extension:

extension UIImageView {
    func downloadedFrom(url: URL, contentMode mode: UIViewContentMode = .scaleAspectFit) {
        contentMode = mode
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard
                let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
                let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
                let data = data, error == nil,
                let image = UIImage(data: data)
            else { return }
            DispatchQueue.main.async() { () -> Void in
                self.image = image
            }
        }.resume()
    }
    func downloadedFrom(link: String, contentMode mode: UIViewContentMode = .scaleAspectFit) {
        guard let url = URL(string: link) else { return }
        downloadedFrom(url: url, contentMode: mode)
    }
}

Usage:

imageView.downloadedFrom(link: "image url")

For your question

Do this while you are loading the images in your table view cell:

 cell.imageView?.downloadedFrom(link: productLibrary[mainPlaces[indexPath.row]]! )
Samarth Kejriwal
  • 1,168
  • 2
  • 15
  • 30
  • Hi @Samarth, thanks for the reply, but I'm not understanding how this code downloads the images and stores them into a new dictionary along with the product as the key for me to use in the UITableView method. This seems to cater for a single URL, where I have 128 URL's linked to 128 products, which get filtered based on the user's location to a beacon, so the TableView is constantly updating and needs to display the appropriate images at the time of update.\ – GarrethTrent Jul 19 '17 at 19:02
  • Could you perhaps advise how I would implement the above code specifically into a UITableView (cellForRowAt) method, where I reference the product name from an array related to the indexPath.row, and then the image from a dictionary with the array[indexPath.row] as the key (specified in my original post). I really appreciate the assistance. – GarrethTrent Jul 19 '17 at 19:13
  • @GarrethTrent you are getting the products name in your table view cells? – Samarth Kejriwal Jul 20 '17 at 04:27
  • @GarrethTrent i have added some lines of code at the end.please see it – Samarth Kejriwal Jul 20 '17 at 04:51
  • Thanks so much for the replies @Samarth, I really appreciate the assistance. I'm only getting 1 error now in the initial code you provided above. It's in the "self.imageView.image = UIImage(data: data)" line of code, and the error is "value of type 'ViewController' has no member 'imageView'. Also, if I implement your added line in the UITableView method, it gives me the error "Cannot convert value of type 'String' to expected argument type 'URL'. Should I be putting this on its own in the method, or as part of the cell.imageView?.image = "Added Code"? Thanks again for the help! – GarrethTrent Jul 20 '17 at 05:21
  • @GarrethTrent use the extension in then to load the images in your table view cell – Samarth Kejriwal Jul 20 '17 at 05:52
  • You might also use the previous code by editing it because it wants a url as the argument, then check how I have changed the string URL into `checkedurl` which is of type URL – Samarth Kejriwal Jul 20 '17 at 06:06
  • Hi @Samarth. It's still not displaying the images. I've edited the post to include my full code of the table view methods and the inclusion of your additional methods advised. Would you mind taking a look and letting me know where I've gone wrong? – GarrethTrent Jul 20 '17 at 07:48
  • @GarrethTrent remove `downloadImage(url: URL(string: productLibrary[mainPlaces[indexPath.row]]!)!) ` from your code, you are already loading the image using `cell.imageView?.downloadedFrom(link: productLibrary[mainPlaces[indexPath.row]]!)`. Tell me what happened then. – Samarth Kejriwal Jul 20 '17 at 08:14
  • Hi @Samarth, tried to move this to a chat, but my reputation isn't high enough yet. I removed the line, but it still didn't load the images. I don't have any further code in the ViewDidLoad method as per your code above, as this was, in my understanding, implemented in the tableView method. Would I need to put any code into the ViewDidLoad method as well? – GarrethTrent Jul 20 '17 at 08:24
  • Do one thing try to check whether you are getting the image urls in your array or not .print that image url array – Samarth Kejriwal Jul 20 '17 at 08:26
  • the mainPlaces array gets created from a dictionary, but in essence is just an array of product names, i.e. mainPlaces = ["MacBook Air 13","MacBook Pro Retina"], etc. I then have a dictionary of the image URLs linked to the product names, productLibrary, which would look like this: productLibrary = ["MacBook Air 13":"http://www.link.com/image.jpg","MacBook Pro Retina":"http://www.link.com/image2.jpg"] etc. In the tableView it then references the mainPlaces' location for the indexPath.row and applies it to the productLibrary to fetch the URL. – GarrethTrent Jul 20 '17 at 08:43
  • I'm also not sure where to print the image url array from, as this isn't something I've specified. The only image variable I can locate is in the extension portion of your code, but it doesn't seem to be referenced in any of the other methods... – GarrethTrent Jul 20 '17 at 08:46
  • Are you getting any error in the code while loading the images ? – Samarth Kejriwal Jul 20 '17 at 09:00
  • The app runs fine, but just displays the text in the cells without the images, so I don't get any errors. When I had the `downloadImage` command in, the "Download Started" message, image name and "Download Finished" message printed in the console as per your `downloadImage` method, but since removing it, nothing prints to the console at all. – GarrethTrent Jul 20 '17 at 09:07
  • @GarrethTrent share your project I will see to it: samarth220194@gmail.com – Samarth Kejriwal Jul 20 '17 at 09:23
  • There might be something wrong with the connection or other thing. I will check it if your permit me to on your project – Samarth Kejriwal Jul 20 '17 at 09:31
  • 1
    Hi @Sarmath. Thanks for the offer, but the project is quite big (152MB) to mail. I've notice that when I press on the cell, just before it performs thew Segue to the second view Controller, the image displays in the cell. If I touch and drag down to not perform the Segue, the image displays for a moment, and then disappears. This is the reason why I wanted to load all of the images into a dictionary in the ViewDidLoad method, which can be accessed at any time through the tableView method. I hope this helps to resolve. By the way, thanks very much for taking the time to help me. – GarrethTrent Jul 20 '17 at 10:22
  • @GarrethTrent Feel free to ask anytime. SO is the best platform. Sorry i couldn't solve your problem this time – Samarth Kejriwal Jul 20 '17 at 11:12
  • See the way you implemented very initially in your question was perfectly correct. I came to know about this after watching a tutorial. I think there must be some issue with your storyboard outlet connection or something – Samarth Kejriwal Jul 20 '17 at 11:18
  • @GarrethTrent Follow this tutorial and everything will be perfect : https://www.youtube.com/watch?v=ea6_a_zbQrY&t=1403s – Samarth Kejriwal Jul 20 '17 at 11:23
  • Thanks very much, I'll check it out. – GarrethTrent Jul 20 '17 at 13:27
  • @GarrethTrent it contains all the info you need. Please let me know if it solves your problem – Samarth Kejriwal Jul 20 '17 at 13:30
  • @SamarthKejriwal : Getting this error : TIC SSL Trust Error. Could you figure out? – Jayprakash Dubey Jan 30 '18 at 05:45
0

I managed to get this right using a dataTask method with a for loop in the viewDidLoad method, which then updated a global dictionary so it didn't have to repopulate when I switched viewControllers, and because it isn't in the tableView method, the table remains responsive while the images load. The url's remains stored as a dictionary linked to the products, and the dictionary then gets populated with the actual UIImage as a dictionary with the product name as the key. Code as follows:

if images.count == 0 {
    for product in productLibrary {

        let picurl = URL(string: product.value)

        let request = NSMutableURLRequest(url: picurl!)

        let task = URLSession.shared.dataTask(with: request as URLRequest) {
            data, response, error in

            if error != nil {

                print(error)

            } else {

                if let data = data {

                    if let tempImage = UIImage(data: data) {

                        images.updateValue(tempImage, forKey: product.key)

                    }

                }


            }

        }

        task.resume()


    }

    }

I hope this helps, as this is exactly what I was hoping to achieve when I asked this question and is simple to implement.

Thanks to everyone who contributed.