Matt Huszagh Matt Huszagh - 1 month ago 19
Swift Question

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!

Answer

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
        }
    }