Blake Blake - 7 months ago 24
Swift Question

Return NSData type asynchronously

I'm learning swift and trying to implement a function to download return an NSData type, whilst also saving it to another variable within the class.

I would prefer answers that don't radically change the code explained below as this is how we have been 'told to do it' for now.

I can't figure out where to properly return the newly downloaded NSData type as it's within the dispatch_async() function, and given that it downloads in the background, the function returns sooner than it's downloaded so the calling function sees it return nil...

Here's my class (simplified) -

class Photo {
var title : String
var url : String
var data : NSData? = nil

func imageData(newUrl: String? = nil) -> NSData? {
/// Check if the current Photo object has either:
/// - No data cached -OR-
/// - The URL differs from newUrl variable
if self.data == nil || (newUrl != nil && newUrl != self.url) {

if let url = NSURL(string: self.url) {
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)

// Asyncronous block
dispatch_async(queue) {
if let newData = NSData(contentsOfURL: url) {
let mainQueue = dispatch_get_main_queue()
dispatch_async(mainQueue, {
self.data = newData // Cache data
})
} else {
print("Could not download image: '\(self.url)'")
self.data = nil
}
}
}
}
return self.data
}

} // End class

Answer

One way to do this is by implementing a completion handler. Simply put you don't actually return anything from the function but simply pass any data to the completion handler which then calls the handler defined elsewhere.

In your example you can change the function definition to func imageData(newUrl: String? = nil, completion: (data: NSData?) -> Void). You have now defined a completion handler (a closure) called completion with a data parameter and a return type of Void. You can then define the handler to this block where you call this function, like this:

imageData("someurl") { (data) in
     print(data)
}

This will simply print the data to the console, but you can do whatever you want with this data.

Full code:

    func imageData(newUrl: String? = nil, completion: (data: NSData?) -> Void) {
    /// Check if the current Photo object has either:
    ///     - No data cached -OR-
    ///     - The URL differs from newUrl variable
    if self.data == nil || (newUrl != nil && newUrl != self.url) {

        if let url = NSURL(string: self.url) {
            let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)

            // Asyncronous block
            dispatch_async(queue) {
                if let newData = NSData(contentsOfURL: url) {
                    let mainQueue = dispatch_get_main_queue()
                    dispatch_async(mainQueue, {
                        self.data = newData // Cache data
                        // pass data to the completion block
                        completion(data: self.data)
                    })
                } else {
                    print("Could not download image: '\(self.url)'")
                    self.data = nil
                }
            }
        }
    } else {
        // pass data to the completion block
        completion(data: self.data)
    }
}

There are other ways to achieve this, but I think this will be the easiest and with minimal code changes. Other ways you could look up are using delegates, NSNotification or NSOperation. For more information, please take a look at these tutorials: http://www.appcoda.com/ios-concurrency/, https://www.raywenderlich.com/79149/grand-central-dispatch-tutorial-swift-part-1, https://www.raywenderlich.com/76341/use-nsoperation-nsoperationqueue-swift