cesarcarlos cesarcarlos - 5 months ago 28
JSON Question

How do I get a value from an NSURLSession task into an instance variable?

I have a tableView which I want to fill with a list of items provided by a web service. The service returns a JSON object with status (success or failure) and shows (an array of strings).

In viewDidLoad I call the custom method getShowsFromService()

func getShowsFromService() {
// Send user data to server side
let myURL = NSURL(string: "https://myurl.com/srvc/shows.php")

// Create session instance
let session = NSURLSession.sharedSession()

var json:NSDictionary = [:]

// Create the task
let task = session.dataTaskWithURL(myURL!) { //.dataTaskWithRequest(request) {
(data, response, error) in

guard let data = data else {
print("Error: \(error!.code)")
print("\(error!.localizedDescription)")
return
}

do {
json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as! NSDictionary
} catch {
print (error)
}

let sts = json["status"] as! NSString
print("\(sts)")
}

// Resume the task so it starts
task.resume()

let shows = json["shows"] as! NSArray
for show in shows {
let thisshow = show as! String
showsArray.append(thisshow)
}

// Here I get "fatal error: unexpectedly found nil while unwrapping an Optional value"

}


The method receives the JSON object and puts it into a dictionary. Then I want to use that dictionary to call json['shows'] in order to get to the array of shows which I want to store in an instance variable called showsArray. The idea is to use showsArray in tableView(cellForRowAtIndexPath) in order to fill in the data.

The problem is that I can't get the Dictionary into the variable. If I try to do it inside the task, I get an error that says I need to call self.showsArray and if I do, the data doesn't go inside the array. If I do it outside the task I get an error because it says I'm trying to force unwrap a nil value.

How can I get the Dictionary created within the task out into the showsArray var?

Answer

The dataTaskWithURL method makes an async call, so as soon as you do task.resume() it will jump to the next line, and json["shows"] will return nil as the dictionary is empty at this point.

I would recommend moving that logic to a completion handler somewhere in your class. Something along the lines of:

func getShowsFromService() {
    let myURL = NSURL(string: "https://myurl.com/srvc/shows.php")
    let session = NSURLSession.sharedSession()
    let task = session.dataTaskWithURL(myURL!, completionHandler: handleResult)
    task.resume()
}

//-handle your result
func handleResult(data: NSData?, response: NSURLResponse?, error: NSError?) {
    guard let data = data else {
        print("Error: \(error!.code)")
        print("\(error!.localizedDescription)")
        return
    }

    do {
        if let json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as! NSDictionary {
            if let shows = json["shows"] as! NSArray {
                //- this is still in a separate thread
                //- lets go back to the main thread!
                dispatch_async(dispatch_get_main_queue(), {
                    //- this happens in the main thread
                    for show in shows {
                        showsArray.append(show as! String)
                    }
                    //- When we've got our data ready, reload the table
                    self.MyTableView.reloadData()
                    self.refreshControl?.endRefreshing()
                });
            }
        } 
    } catch {
        print (error)
    }
}

The snippet above should serve as a guide (I dont have access to a playground atm).

Note the following: as soon as the task completes (asynchronously -> different thread) it will call the new function handleResult which will check for errors and if not, it will use the dispatcher to perform your task on the main thread. I'm assuming showsArrays is a class property.

I hope this helps

EDIT:

As soon as you fetch your data you need to reload the table (updated code above). You can use a refresh control (declare it as a class property).

var refreshControl: UIRefreshControl!

Then when you finish getting your data you can refresh:

self.MyTableView.reloadData()
self.refreshControl?.endRefreshing()

This will call your delegate methods to populate the rows and sections.