Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Microsoft Azure (or Swift in general) fail to update a variable to return after a table query?

I've been following the Microsoft Azure documentation on querying tables successfully (inserting, reading, and updating items into the database work fine), but at the end of a simple method, right off the docs:

func getAllEventIDs() -> [String] {
    var events:[String] = [] //this is to be updated
    let delegate = UIApplication.sharedApplication().delegate as! AppDelegate
    let client = delegate.client! //boiler-plate for azure

    let itemTable = client.tableWithName("Events")
    itemTable.query().readWithCompletion { 
    //something about this query block might be preventing successful initialization to the events array
        (result:MSQueryResult!, error) in
        //usually error-checking here
        for item in result.items {
            events.append(item.valueForKey("id") as! String)
            //returning events here...
        }
        //...and here (almost) work, since Swift expects a return of type Void
    }
    return events //still empty
}

I may not pass the array in as a parameter, since the .append function will mutate that array.

The returned value is nothing but the initial empty array. However, this issue seems to stem heavily from Azure's query code block and not from Swift itself.

A simpler example:

func returnValueArray() -> [Int] {
    var array:[Int] = [0,0,0,0]
    var num:Int = 3
    for var n = 0; n < array.count; n++ {
        array[n] = num
    }
    return array
}

This returns [3,3,3,3]. Again, not Swift's problem, but Swift may have manifested Azure's return issue.

How can I return the desired, updated array at the end of query method? Is it possible to pass in a mutable array into the method, append values, and then return that array?

like image 931
Evo Sae Avatar asked Aug 10 '15 02:08

Evo Sae


1 Answers

You asked: "How can I return the desired, updated array at the end of query method?" Short answer: You can't.

This is a fundamental of asynchronous coding.

The readWithCompletion method is asynchronous. It queues your request for processing in the background, and the returns immediately.

Your return events //still empty code executes before your read request has even begun processing.

You need to refactor your getAllEventIDs method to take a completion block as a parameter. That completion block would be passed your events array. Then inside the completion block for readWithCompletion you would call the completion block for your getAllEventIDs method.

So when you call getAllEventIDs, you pass it a completion block that does what you need to do with the events array.

EDIT:

I created a Github project called SwiftCompletionHandlers that illustrates this and how to handle it. It has a sample class AsyncManager which simulates asynchronous downloading.

https://github.com/DuncanMC/SwiftCompletionHandlers

It has a method that looks like this:

func asyncFetchImage(#imageName: String,
  completion: (
    image: UIImage?,
    status: String) -> ())
  {
    println("Entering \(__FUNCTION__)")
    
    //Simulate a network operation by waiting a few seconds 
    //before loading an image
    let nSecDispatchTime = dispatch_time(DISPATCH_TIME_NOW, Int64(3.0 *
      Double(NSEC_PER_SEC)))
    let queue = dispatch_get_main_queue()
    dispatch_after(nSecDispatchTime, queue)
      {
        () -> Void in
        let result = UIImage(named: imageName)
        println("Loading image in background")
        let status = result != nil ? "image loaded" : "Error loading image"
        println("About to call completion handler")
        completion(image: result, status: status)
    }
    println("Leaving \(__FUNCTION__)")
  }

It takes a filename, and a completion block. The completion block gets passed a UIImage optional and a String status message.

Once the download is complete, the method invokes the completion block (aka closure.)

Here is the code that calls this method:

  @IBAction func loadImage(sender: UIButton)
  {
    let theAsyncManager = AsyncManager.sharedAsyncManager
    println("about to call asyncFetchImage")
    theAsyncManager.asyncFetchImage(imageName: "wareto_blue_150x115")
      {
        (image, status) -> () in
        println("Beginning completion block")
        self.theImageView.image = image
        println("In completion block, status = \(status)")
    }
    println("After call to asyncFetchImage")
  }

The output of the println statements is key to understanding what's going on:

about to call asyncFetchImage
Entering asyncFetchImage(imageName:completion:)
Leaving asyncFetchImage(imageName:completion:)
After call to asyncFetchImage
Loading image in background
About to call completion handler
Beginning completion block
In completion block, status = image loaded

Note that the "Leaving asyncFetchImage" and "After call to asyncFetchImage" messages print before the "Loading image in background" message. Then comes "About to call completion handler", then "Beginning completion block".

So the actual async work doesn't even start until after the loadImage function has returned.

If you don't understand what I'm describing, download the project and try it out, then set breakpoints and watch it execute.

like image 131
Duncan C Avatar answered Nov 14 '22 23:11

Duncan C