0

I'm trying to retrieve images from array of url..

I have this function that do the same as I won't but it doesn't work so I tried to use URLSession but didn't know how exactly to make it >>

func downloadImages(imageUrls: [String], completion: @escaping (_ images: [UIImage?]) -> Void) {
        
    var imageArray: [UIImage] = []
    var downloadCounter = 0
    
    for link in imageUrls {
        let url = NSURL(string: link)
        let  downloadQueue = DispatchQueue(label: "imageDowmloadQueue")
        
        downloadQueue.sync {
            downloadCounter += 1
            let data = NSData(contentsOf: url! as URL)
            
            if data != nil {
                //image data ready and need to be converted to UIImage
                imageArray.append(UIImage(data: data! as Data)!)
                
                if downloadCounter == imageArray.count {
                    DispatchQueue.main.async {
                        completion(imageArray)
                        
                    }
                }
            } else {
                print("couldnt download image")
                completion(imageArray)
            }
        }
    }
}

The function I work on :

public func imagesFromURL(urlString: [String],completion: @escaping (_ images: [UIImage?]) -> Void) {
        
        var imageArray: [UIImage] = []
        var downloadCounter = 0
        let  downloadQueue = DispatchQueue(label: "imageDowmloadQueue")

        for link in urlString {
            downloadQueue.sync {
            downloadCounter += 1
        let dataTask = URLSession.shared.dataTask(with: NSURL(string: link)! as URL, completionHandler: { (data, response, error ) in

            if error != nil {
                print(error ?? "No Error")
                return
            }
            
            if data != nil {
                imageArray.append(UIImage(data: data! as Data)!)
                if downloadCounter == imageArray.count {
                    completion(imageArray)

                    }
                } else {
                            print("couldnt download image")
                            completion(imageArray)
                           }
                
            } dataTask.resume()
                }
            }
}

i want to call the function in the collection cell and get the display the first image only from each artwork array..


        //download the first image only to display it:
        if artwork.ImgLink != nil && artwork.ImgLink.count > 0 {
            
            downloadImages(imageUrls: [artwork.ImgLink.first!]) { (images) in
                self.artworkImage.image = images.first as? UIImage
            }
        }

NANA
  • 7
  • 5

1 Answers1

0
  1. If you intend to use only the first available UIImage from an array of urls, you do not design a function trying to download all of them. Instead, try to download from the first url, return the downloaded UIImage if it succeeds, or continue with the second url if it fails, repeat until you get an UIImage.
  2. Creating a DispatchQueue in a local function looks dangerous to me. A more common practice is to maintain a queue somewhere else and pass it to the function as a parameter, or reuse one of the predefined global queues using DispatchQueue.global(qos:) if you don't have a specific reason.
  3. Be careful with sync. sync blocks the calling thread until your block finishes in the queue. Generally you use async.
  4. Use a Int counter to control when to finish multiple async tasks (when to call the completion block) works but can be improved by using DispatchGroup, which handles multiple async tasks in a simple and clear way.

Here's two functions. Both work. firstImage(inURLs:completion:) only return the first UIImage that it downloads successfully. images(forURLs:completion:) tries to download and return them all.

func firstImage(inURLs urls: [String], completion: @escaping (UIImage?) -> Void) {
    DispatchQueue.global().async {
        for urlString in urls {
            if let url = URL(string: urlString) {
                if let data = try? Data(contentsOf: url), let image = UIImage(data: data) {
                    DispatchQueue.main.async {
                        completion(image)
                    }
                    return
                }
            }
        }
        DispatchQueue.main.async {
            completion(nil)
        }
    }
}

// Use it.
firstImage(inURLs: artwork.ImgLink) { image in
    self.artworkImage.image = image
}

func images(forURLs urls: [String], completion: @escaping ([UIImage?]) -> Void) {
    let group = DispatchGroup()
    var images: [UIImage?] = .init(repeating: nil, count: urls.count)
    
    for (index, urlString) in urls.enumerated() {
        group.enter()
        DispatchQueue.global().async {
            var image: UIImage?
            if let url = URL(string: urlString) {
                if let data = try? Data(contentsOf: url) {
                    image = UIImage(data: data)
                }
            }
            images[index] = image
            group.leave()
        }
    }
    
    group.notify(queue: .main) {
        completion(images)
    }
}

// Use it.
images(forURLs: artwork.ImgLink) { images in
    self.artworkImage.image = images.first(where: { $0 != nil }) ?? nil
}
Rui L
  • 251
  • 2
  • 7
  • Don't use Data(contentsOf: url) to load data from a non local resource/ Not even from a background thread. – Leo Dabus Sep 16 '20 at 05:16
  • @LeoDabus Can you elaborate on this? Apple suggested not to use Data(contentsOf:) for a remote url because it blocks the current thread: https://developer.apple.com/documentation/foundation/nsdata/1413892-init. What are the reasons for preventing using it from a background thread? – Rui L Sep 16 '20 at 05:52
  • You are using a synchronous initialiser for the wrong purpose. Use the URLSession methods which are the proper methods for asynchronous requests. related https://stackoverflow.com/a/3078783/2303865 – Leo Dabus Sep 16 '20 at 06:18
  • I think you should take some time and read those articles as well [URL Loading System](https://developer.apple.com/documentation/foundation/url_loading_system), [Downloading Files from Websites](https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_from_websites), [Pausing and Resuming Downloads](https://developer.apple.com/documentation/foundation/url_loading_system/pausing_and_resuming_downloads), [Downloading Files in the Background](https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background) – Leo Dabus Sep 16 '20 at 06:33
  • Still not answering the specific question "What are the reasons for preventing using it **from a background thread**?". URLSession is indeed for asynchronous requests (data or file), but **without additional efforts**, it seems have no significant difference from using Data(contentOf:) in a background queue.They both fetch the image once into memory without any optimization, i.e. decoding, cache, invalidation. However, optimization is a much deeper question which beyonds this post and there's libraries addressing it. – Rui L Sep 16 '20 at 08:38
  • Nope downloadTask doesn't load the data to memory. Btw dataTask can not be run in the background and/or be resumed. If Apple did want you to use Data(contentsOf: URL) for async requests they would only say there for you to call it on a background thread instead of telling you to use URLSession. – Leo Dabus Sep 16 '20 at 09:07