3

I'm trying to use multithreading in Swift to retrieve and display an image in a view controller. However, the function I'm using to retrieve the image is in a separate model. I could just copy and paste the function into the view controller, but I will be reusing this function multiple times and would prefer to keep it separate from the specific view controller.

The function I currently have (again, in a separate file I've named WikimediaAPI) is the following:

public func getThumbnailImage(forPage page: String) -> UIImage? {
        if let data = try? Data(contentsOf: URL(string: "https://en.wikipedia.org/w/api.php?action=query&titles=" + page + "&prop=pageimages&format=json&pithumbsize=1000")!) {
            let json = JSON(data: data)
            if let pageId = json["query"]["pages"].dictionary?.first?.key {
                if let imageUrl = json["query"]["pages"][pageId]["thumbnail"]["source"].string {
                    if let url = URL(string: imageUrl) {
                        if let imageData = try? Data(contentsOf: url) {
                            if let image = UIImage(data: imageData) {
                                return image
                            }
                        }
                    }
                }
            }
        }
        return nil
    }

The above works, but is not multithreaded and when I segue to the view controller from a table view controller it takes too long. I've tried to implement multithreading somewhere along these lines, but I am not allowed to return anything within the asynchronous block

public func getThumbnailImage(forPage page: String) -> UIImage? {
        DispatchQueue.global().async {
            if let data = try? Data(contentsOf: URL(string: "https://en.wikipedia.org/w/api.php?action=query&titles=" + page + "&prop=pageimages&format=json&pithumbsize=1000")!) {
                DispatchQueue.main.async {
                    let json = JSON(data: data)
                    if let pageId = json["query"]["pages"].dictionary?.first?.key {
                        if let imageUrl = json["query"]["pages"][pageId]["thumbnail"]["source"].string {
                            if let url = URL(string: imageUrl) {
                                DispatchQueue.global().async {
                                    if let imageData = try? Data(contentsOf: url) {
                                        DispatchQueue.main.async {
                                            if let image = UIImage(data: imageData) {
                                                return image //error: "Unexpected non-void return value in void function"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return nil
    }

I had the thought that it might be possible to return a closure with the asynchronous code block instead of the UIImage, but am not sure how to implement this. And, if I did that, I would still need set the image in the view controller, which I'm not sure how to do.

Here is the call to the function in the view controller:

let wikiQuery = WikimediaAPI() // the class from above

fileprivate func setImage() {
        if let pageTitle = wikimediaPageTitleDict[(allergy.0).lowercased()] {
            if let image = wikiQuery.getThumbnailImage(forPage: pageTitle) {
                wikipediaImage.image = image
                wikipediaImage.contentMode = .scaleAspectFit
            }

        }

  }

I'm using a dictionary (wikimediaPageTitleDict) from a plist to associate a string with the right wikipedia page (allergy.0 is that string)

All help appreciated. Thank you!

MattHusz
  • 452
  • 4
  • 15
  • also, JSON above refers to SwiftyJSON (https://github.com/SwiftyJSON/SwiftyJSON) – MattHusz Oct 25 '16 at 23:57
  • 2
    Just a heads up, you can chain your `if lets` into the same condition to avoid the pyramid of death. – JAL Oct 26 '16 at 00:49

3 Answers3

1

Both AlamofireImage and SDWebImage do this for you and they extend UIImageView so you just call a method on the image view and when the image is downloaded (or retrieved from the cache) the image view gets updated. I suggest you use a library for this. But if you want to do it yourself (note the guards to eliminate the if let pyramid of doom):

    typealias ImageCallback = (UIImage?) -> Void

    func getThumbnailImage(forPage page: String, completion: @escaping ImageCallback) -> void {
        DispatchQueue.global().async {
            var imageToReturn: UIImage? = nil
            defer {
                completion(imageToReturn)
            }
            guard let data = try? Data(contentsOf: URL(string: "https://en.wikipedia.org/w/api.php?action=query&titles=" + page + "&prop=pageimages&format=json&pithumbsize=1000")!) else {
                return
            }
            let json = JSON(data: data)
            guard let pageId = json["query"]["pages"].dictionary?.first?.key,
                  let imageUrl = json["query"]["pages"][pageId]["thumbnail"]["source"].string,
                  let url = URL(string: imageUrl) else {
                return
            }
            guard let imageData = try? Data(contentsOf: url),
                  let image = UIImage(data: imageData) else {
                return
            }
            imageToReturn = image
        }

    }

    getThumbnailImage(forPage: "Escherichia_coli") { image in
        DispatchQueue.main.async {
            imageView.image = image
        }
    }
Josh Homann
  • 15,933
  • 3
  • 30
  • 33
  • Good point, I forgot that Alamofire had built in async image loading. This is by far the easiest and best answer you're already using Alamofire – Duncan C Oct 26 '16 at 00:28
  • thanks! I ended up using Alamofire, but the demo code was really helpful for me to understand how this should normally be done. – MattHusz Oct 26 '16 at 01:38
  • also appreciate the guard structure, didn't realize I could do that, but looks way cleaner – MattHusz Oct 26 '16 at 01:39
0

If you are using async code, you can't return a result from your function. That's not how async works. An async function call queues up the result to be fetched sometime later. The function then returns before the results are available.

What you usually do with async functions is to pass in a completion block that gets called when thee results are available.You might pass in a closure that installs the newly downloaded image into the appropriate image view in your view controller, for example.

See my answer on this thread.

Storing values in completionHandlers - Swift

I give a working example of an async function that takes a completion handler.

Community
  • 1
  • 1
Duncan C
  • 128,072
  • 22
  • 173
  • 272
0

You cannot return anything within a dispatch async closure. You can however pass the UIImageView variable to your getThumbnailImage and when you receive the image, you can set it right away.

public func getThumbnailImage(forPage page: String, imageView : UIImageView) 
{
  ...
  ...
         if let imageData = try? Data(contentsOf: url) {
            DispatchQueue.main.async {
              if let image = UIImage(data: imageData) {
                imageView.image = image                                 
              }
            }
         }
}
Christian Abella
  • 5,747
  • 2
  • 30
  • 42