2

I have a question that might be not specifically about implementation but rather a tip/best practice thing.

I am working on a class in Swift that gets data from an online source in JSON format. I want to have specific methods in this class that connect to the online source and give the result back in a Dictionary type. The function might be like this:

func getListFromOnline()->[String:String]{

    var resultList : [String:String]=[:]
    ...
    /*
    Some HTTP request is sent to the online source with NSURLRequest

    Then I parse the result and assign it to the resultList
    */
    ...

    return resultList
}

What I get right now in this implementation, without the multithreading, the resultList is apparently returned before fetching from the online source is done. Not surprisingly, it leads to failure in the program.

Any idea or tip how I can achieve the opposite? I am a bit confused with multithreading in this instance, because I want to call this public method asynchronously later from another class and I know how to do that, but I don't get how to make the method that returns an argument have multithreading within itself.

Or is it not about multithreading at all and I am not seeing an obvious solution here?

ABakerSmith
  • 21,863
  • 9
  • 66
  • 77
lakeoffm
  • 73
  • 1
  • 1
  • 5

3 Answers3

7

There are three easy and completely expect approaches to this problem in Swift/Objective-C development, and neither of these solutions involve the method returning the value directly. You could write code that waited for the asynchronous part to complete (blocking a thread) and then return the value, and there are cases where this is done in some of Apple's own libraries, but I'm not going to cover the approach, because it's really not that great of an idea.


The first approach involves completion blocks.

When our method is going to perform some asynchronous code, we can pass in a block of code to execute whenever the asynchronous work is done. Such a method would look like this:

func asynchStuff(completionHandler: ([String:String]) -> Void) {
   // do asynchronous stuff, building a [String:String]
   let result: [String: String] = // the result we got async
   completionHandler(result)
}

Remember to call completionHandler() on the same thread that asynchStuff was called on. (This example does not demonstrate that.)


The second approach involves delegates and protocols.

We need a class to do the asynchronous work. This class will hold a reference to our delegate, which will implement completion methods. First, a protocol:

@objc protocol AsyncDelegate {
    func complete(result: [String:String]
}

Now our async worker:

class AsyncWorker {
    weak var delegate: AsyncDelegate?

    func doAsyncWork() {
        // like before, do async work...
        let result: [String: String] = // the result we got async
        self.delegate?.complete(result)
    }
}

Remember to be certain that we're calling the completion delegate method on the same thread that doAsyncWork() was called on. (This example does not demonstrate that.)


The third approach can be done using NSNotificationCenter. The times when this is the appropriate approach are going to be so rare that I'm not even going to bother with even a rudimentary example like I did the other two examples because you should almost certainly be using one of the first two examples in almost every scenario.


Which approach you use depends entirely on your specific use case. In Objective-C, I frequently preferred delegation to block based approach (although sometimes blocks are correct), but Swift allows us to pass regular functions/methods as our block argument so it leans me slightly back toward using the block-based approach for Swift, but still, use the right tool for the right job, as always.


I wanted to expand this answer to address calling the callback (whether blocks or delegates) on the appropriate thread. I'm not sure if there's a way to do this with GCD still, but we can do it with NSOperationQueues.

func asyncStuff(completionHandler: ([String:String]) -> Void) {
    let currentQueue = NSOperationQueue.currentQueue()
    someObject.doItsAsyncStuff(someArg, completion: { 
        result: [String:String] in
        currentQueue.addOperationWithBlock() {
            completionHandler(result)
        }
    }
}
nhgrif
  • 58,130
  • 23
  • 123
  • 163
  • 1
    Depends on whether you are using GCD or NSOperations. I can't update answer right now (on phone), but I can update later for sure. @GoZoner – nhgrif Jun 27 '15 at 22:25
  • GCD (I thought the 'get current queue' function was deprecated. See: http://stackoverflow.com/a/17678556/1286639). – GoZoner Jun 27 '15 at 22:33
  • 1
    You can always use NSOperationQueues still... I will update this answer either way, when I get a chance to do a bit of my own research. – nhgrif Jun 27 '15 at 23:01
  • 1
    @GoZoner GCD `dispatch_get_current_queue` is deprecated, but `currentQueue` of `NSOperationQueue` is not deprecated. Having said that, `NSURLSession` and AFNetworking avoid this problem completely by (a) having a parameter/property for which queue to run the completion blocks; and (b) have some predefined behavior if this queue is `nil` (with `NSURLSession`, it will default to its own queue, with AFNetworking it defaults to main queue). – Rob Jun 29 '15 at 00:58
  • Thanks for the comments! I will probably use the closure for the code, but I still have a question to that. If I use a closure in a class that connects to the online source, how do I pass data obtained to the class that has invoked that class with async methods? For instance, if I have a ViewController class that invokes an Async class, how do I pass the data back to ViewController? – lakeoffm Jun 29 '15 at 09:27
  • I mean, I could have easily had those connection methods in my ViewController and display the results in the UI asynchronously, but what if I want to keep those methods in a separate class? – lakeoffm Jun 29 '15 at 09:28
  • I don't understand what you're asking, but you cannot update the UI asynchronously--you can only update the UI from the main thread. – nhgrif Jun 29 '15 at 11:53
  • 1
    @lakeoffm If you're asking how to update the UI as the data is received and processed, then in addition to a "completion" block or delegate method, like nhgrif said, you might also have a "progress" block or delegate method, too. See AFNetworking for block-based example. See delegate protocols for `NSURLConnection` or `NSURLSession` for examples of delegate-protocol-based example. By the way, you should consider [accepting nhgrif's answer](http://stackoverflow.com/help/someone-answers). Also, don't ask new questions in comments, but (a) try it; and (b) post new question if needed. – Rob Jun 29 '15 at 14:17
  • "Remember to call completionHandler() on the same thread that asynchStuff was called on" - why is this useful? In your example, `completionHandler(result)` will be called on whatever thread where `asynchStuff` is executed on. Presumable, this is some private thread provided by GCD - iff `asynchStuff` has been dispatched with `dispatch_async()` for example. I would think explicitly NOT using this queue (because it might be some dedicated queue especially appropriate for executing `asyncStuff`, for example an "IO bound queue) but instead one of GCD's global queues is a much better approach. – CouchDeveloper Jul 04 '15 at 13:48
  • My example code snippet does not show the code required to do what I was commenting about (hence the comment). The last snippet, which used `NSOperationQueue` is what I meant by that comment. I will try to make it more clear. – nhgrif Jul 04 '15 at 13:49
  • In your last example using `NSOperationQueue` we HAVE to assume, that the call-site is a `NSOperation` executing on a `NSOperationQueue`. Otherwise, your code will not work since that class method `currentQueue()` returns an Optional which will be `nil`. And still, I don't see the benefit executing the completion on the call-site's NSOperationQueue: the call-site can always easily dispatch to any appropriate queue which executes the _actual_ code of the completion handler. Then again, if choosing a global GCD queue, it would work even in cases where the call-site is not an NSOperation. – CouchDeveloper Jul 04 '15 at 14:10
  • 1
    You're welcome to post your own answer. You're right. The completion *could* dispatch to whatever queue it wants. This isn't about preventing it, but more about making the behavior somewhat expected. The queue asynchronous call should come back to where ever it was dispatched from (unless otherwise specified). The alternatives would be dispatching to a global queue (and documenting that behavior) or accepting a queue to callback on (and calling back on that queue). I've used all of these approaches in the past. – nhgrif Jul 04 '15 at 14:15
  • I'm with you - most people might expect the completion handler to be called from whatever "execution context" the call-site is executing. Unfortunately, this is only possible (as in your example) when solely using NSOperations dispatched to NSOperationQueues. We don't have a way to accomplish this when using GCD queues. :( – CouchDeveloper Jul 04 '15 at 14:22
1

Short answer: You can't. That is not how async processing works. The result will NEVER be available at the time your method returns. In fact your method returns before the async processing even beings.

As ABakerSmith says in his comment, what you should do is to add a completion handler closure (aka block) as a parameter to the function. That is a block of code that the caller passes in. You write your method so that once the async JSON data is done downloading, it invokes the completion block.

Then in your caller you write your code to pass in the code that you want to execute once the download is complete IN THE COMPLETION BLOCK. You have to structure your program so that it can operate without your data (for example, by displaying an empty table view) and the update itself once the data arrives. You might write your completion block to install the data in the data model for a table view and then call reloadData on the table view.

It's generally safer if you write your async method so it performs the completion block on the main thread.

Duncan C
  • 115,063
  • 19
  • 151
  • 241
  • No. Your completion block should be performed on the thread your method was called on. – nhgrif Jun 27 '15 at 21:37
  • @nhgrif and Duncan: I disagree: The completion block should be invoked either on a private "execution context" (Thread, GCD queue, NSOperationQueue, etc.) which is inaccessible from the call-site, or on the "execution context" which has been explicitly specified by the call-site - if there is a parameter for that. This generally avoids potential dead-locks and performance issues. Using the execution context which was current at the call-site is impossible to achieve on Mac OS / iOS - since there is no abstract definition of a "current execution context" which is implicitly given. – CouchDeveloper Jul 04 '15 at 13:28
0

Here an example how to do asynchronous work in a separate thread and then returning back to the main thread in order to update the UI

    let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
    dispatch_async(dispatch_get_global_queue(priority, 0)) {

        // do some asynchronous work

        dispatch_async(dispatch_get_main_queue()) {

            // use returned data to update some UI on the main thread
        }
    }
Ivan Rigamonti
  • 686
  • 5
  • 7