Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift - Use multithreading from function in a separate file

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!

like image 706
MattHusz Avatar asked Dec 21 '25 17:12

MattHusz


1 Answers

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
        }
    }
like image 186
Josh Homann Avatar answered Dec 23 '25 08:12

Josh Homann