milo526
milo526

Reputation: 5083

Return value from asynchronous thread

In a playground file, I have a function which is supposed to return a array where Lesson is a custom data type. In the playground at the right hand of my screen it does display a array with the values that where supposed to be in the array though if I try to assign the returned array to a variable for further use, the array seems to be empty.
I’ve read around the web that this has to do with NSURLSession.sharedSession() being a asynchronous thread soo my array being returned before the function has gotten al the data.

Code:

//
//  CCApp
//
//  Created by Milo Cesar on 02-03-15.
//  Copyright (c) 2015 Experium. All rights reserved.
//

import Foundation
import XCPlayground

/*
Custom Data Types
Day:    Used to easily transition from day to int
Lesson: Used to store all the necessary info to be displayed to the user
*/

//Day Enum
enum Day: Int{
case Maandag = 0, Dinsdag, Woensdag, Donderdag, Vrijdag
}

//Lesson Struct
struct Lesson {
var day:Day
var start:String
var end:String
var lesson:String
var room:String
var teacher:String
var groups:String
var type:String
var isBreak:Bool
var isCanceled:Bool
}

/*
Code from medium.com used to get & parse JSON
source: https://medium.com/swift-programming/learn-nsurlsession-using-swift-ebd80205f87c
*/
func httpGet(request: NSURLRequest!, callback: (String, String?) -> Void) {
var session = NSURLSession.sharedSession()
var task = session.dataTaskWithRequest(request){
    (data, response, error) -> Void in
    if error != nil {
        callback("", error.localizedDescription)
    } else {
        var result = NSString(data: data, encoding:
            NSASCIIStringEncoding)!
        callback(result, nil)
    }
}
task.resume()
}

func parseJSON(inputData: NSData) -> Array<AnyObject>{
var error: NSError?
var boardsDictionary = NSJSONSerialization.JSONObjectWithData(inputData, options: NSJSONReadingOptions.MutableContainers, error: &error) as Array<AnyObject>

return boardsDictionary
}

/*
Code from medium.com mixed with own input and adaptations.
*/

var request = NSMutableURLRequest(URL: NSURL(string: "http://api.ccapp.it/v1/student/110919/schedule/10")!)


func loadLesson() -> Array<Lesson>{
var loadedLesson:Array<Lesson> = []

//Get JSON Data using http://api.ccapp.it/v1/student/110919/schedule/10 as JSON location
httpGet(request) {
    (data, error) -> Void in
    //Check for Errors
    if error != nil {
        println(error)
    } else {
    //If there are no errors debug (print data) and transistion from JSON to Lesson Struct
        println(data)
        loadedLesson = setupLesson(data)
    }
}
return loadedLesson
}

func setupLesson(data: String) -> Array<Lesson>{
//Clear Array when we get load a new schedule
var loadedLesson:Array<Lesson> = []

//Encode NSString to NSData
let jsonData = data.dataUsingEncoding(NSASCIIStringEncoding)

//parse NSData and return Array<AnyObject>
    //AnyObject is here a Array<NSDictionary>
var jsonArray = parseJSON(jsonData!)

//Loop through the avaidable days in the schedule
for days in 0...jsonArray.count-1{
    //Validate the before stated Array<NSDictionary>
    if let day: AnyObject = jsonArray[days] as? Array<NSDictionary>{
        //I think this line and the previous line should be able to be one line of code but I got no clue how so just cast it as a Array<NSDictionary> (again?)
        if let dayArray:Array<NSDictionary> = day as AnyObject? as Array<NSDictionary>? {

            //Loop through the lessons of the day
            for lessonList in 0...dayArray.count-1{
                //Get all values and store them in a local variable for easy access
                var day:Day
                var start:String = dayArray[lessonList].valueForKey("start") as String
                var end:String = dayArray[lessonList].valueForKey("end") as String
                var lessonID:String = dayArray[lessonList].valueForKey("lesson") as String
                var room:String = dayArray[lessonList].valueForKey("room") as String
                var teacher:String = dayArray[lessonList].valueForKey("teacher") as String
                var groups:String = dayArray[lessonList].valueForKey("groups") as String
                var type:String = dayArray[lessonList].valueForKey("type") as String
                var isBreak:Bool = dayArray[lessonList].valueForKey("break") as Bool
                var isCanceled:Bool = dayArray[lessonList].valueForKey("canceled") as Bool

                //Create a new Lesson Value for easy storage
                var lesson = Lesson(day: Day.Maandag, start:start, end:end, lesson:lessonID, room:room, teacher:teacher, groups:groups, type:type, isBreak:isBreak, isCanceled:isCanceled)

                //Add the lesson to the array
                loadedLesson.append(lesson)
            }
        }
    }
}
//return the now filled array of lessons
return loadedLesson
}

//Receive a empty Array List
var lessonList = loadLesson()
lessonList.isEmpty

XCPSetExecutionShouldContinueIndefinitely(continueIndefinitely: true)

Upvotes: 1

Views: 2212

Answers (2)

Sam
Sam

Reputation: 5014

You need to put everything that you want to execute after the request finishes into the completion block. You're returning loadedLesson before the request has a chance to finish, so it returns an empty array.

One way to handle this is to call another function when the data is loaded.

For example:

func finishedLoading(array: Array<Lesson>) {
    // Do something with the array
}
func loadLesson() {
    //Get JSON Data using http://api.ccapp.it/v1/student/110919/schedule/10 as JSON location
    httpGet(request) {
        (data, error) -> Void in
        //Check for Errors
        if error != nil {
            println(error)
        } else {
        //If there are no errors debug (print data) and transistion from JSON to Lesson Struct
            println(data)
            finishedLoading(setupLesson(data))
        }
    }
}

Side note:

Instead of for lessonList in 0...dayArray.count-1, you can use ..<, which will stop iteration at 1 before the end of the array:

 for lessonList in 0..<dayArray.count {
     // inner loop
 }

Upvotes: 1

kellanburket
kellanburket

Reputation: 12853

That's correct: Your HTTP request is asynchronous, which means that the request runs on a background thread while allowing user interface actions and whatever else to continue running unimpeded on the main thread. In order to get your resulting data you can pass a anonymous callback function as a parameter to loadLesson, that will return your array when the request has been fully processed:

func loadLesson(onComplete: ([Lesson]) -> ()) {

    httpGet(request) {
        (data, error) -> Void in

        if error != nil {
            println(error)
        } else {
            let lessons = setupLesson(data)
            onComplete(lessons)
        }
    }
}

loadLesson { (lessons) in
    for lesson in lessons {
        println(lesson)
    }
}

Now, instead of attempting to access your data right away, the block you passed as a parameter to loadLesson waits to execute until it is called from within httpGet.

Upvotes: 3

Related Questions