1

I've been trying to understand how callbacks work in Swift. I've gone over quite a few examples (like this one) that have helped me to better understand callbacks, but I haven't had any luck in properly implementing one.

I have a function that accepts a URL, downloads some XML data from a web api and then parses it into objects. At the same time I have a UILabel that is waiting for some data from the XML request.

Below is a partial example of my function that I'd like to set up with a callback. For the sake of clarity just assume it only returns a single data point which which will be assigned to a UILabel later:

XMLUtility.swift

// global
var weekForecasts = [DayForecast]()

class XMLUtility {

    func retrieveDataFromXML(myUrl: String) {

        if let url = NSURL(string: myUrl) {
            if let data = NSData(contentsOfURL: url) {
                var error: NSError?
                var cleanedData = filterData(data)

                if let doc = AEXMLDocument(xmlData: cleanedData, error: &error) {

                //... does some work parsing xml ////


                for day in date {

                   //... some work assigning values /////

                   weekForecasts.append(thisDay)

                }         
            }   
        }  
    } 

The problem occurs in my ViewController... I have some UILabels that are waiting for values from the XML data request. When the ViewController loads, the XML hasn't processed yet and the label failed to receive a value.

Here's a simplified example of what I am doing in my ViewController:

ViewController.swift

 @IBOutlet weak var currentTemperatureLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()



    currentTemperatureLabel.text = // I get a value out of my [DayForecasts]
}

I understand why this is the case, and I have a novice understanding of how to solve the problem. I believe I need to use a callback but, based on the examples I have seen so far, I am not sure how to implement one.

My question:

Given the example provided, how would I convert my retrieveDataFromXML method into a callback. Additionally, how do I call the function from my ViewController to access the data.

Any help on this would be greatly appreciated!

Community
  • 1
  • 1
Dan Beaulieu
  • 19,406
  • 19
  • 101
  • 135
  • i've figured out a temporary solution using global variables but I'd really like to understand closures and callbacks better. – Dan Beaulieu Apr 26 '15 at 03:09
  • Have you looked at `NSURLSession`? It is a Cocoa class that requests the data from the URL and allows you to provide a completion handler to handle the data after it is retrieved from the URL. That may be a better option than trying to re-invent callback functionality on your own. The method you probably want to look at is `dataTaskWithURL:completionHandler:` Here is a link: https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSession_class/#//apple_ref/occ/instm/NSURLSession/dataTaskWithURL:completionHandler: – Aaron Rasmussen Apr 28 '15 at 19:51
  • If I have time later I could provide a coded example, but thought I'd at least suggest that you look in that direction. – Aaron Rasmussen Apr 28 '15 at 19:52
  • Roman, I've looked at it. To be honest I never understand how to apply what I read about in apple docs until I see production code examples. I've solved the original issue that caused me to post this question but I created a bounty because I want to have a more fundamental understanding of how callbacks work. I would really appreciate a coded example! – Dan Beaulieu Apr 28 '15 at 20:02
  • On a side note, I know I do need to develop the skill to learn from the apple docs. working on it – Dan Beaulieu Apr 28 '15 at 20:06
  • Do you need it to be asynchronous, or do you simply need it to use a callback style? – zneak Apr 28 '15 at 21:36
  • Ultimately asynchronous would probably be best however, right now I'd just like to simply implement it and experiment. Another thing that I'm trying to understand is what I'm supposed to be passing in to the completion handler. – Dan Beaulieu Apr 28 '15 at 21:40

1 Answers1

1
func retrieveDataFromXML(myUrl: String, completion: ((Array<DayForecast>) -> Void)) {

    if let url = NSURL(string: myUrl) {
        if let data = NSData(contentsOfURL: url) {
            var error: NSError?
            var cleanedData = filterData(data)
            var weekForecasts = [DayForecast]() //local variable

            if let doc = AEXMLDocument(xmlData: cleanedData, error: &error) {

                //... does some work creating objects from xml
                for day in date {

                   //... some work assigning values /////

                   weekForecasts.append(thisDay)

                }    
                //pass the local array into the completion block, which takes
                //Array<DayForecast> as its parameter
                completion(weekForecasts) 
            }
        }
    }  
}

called like

//in this example it is called in viewDidLoad
func viewDidLoad() {
    var urlString = "urlstring"
    retrieveDataFromXML(urlString, {(result) -> Void in
        //result is weekForecasts

        //UI elements can only be updated on the main thread, so get the main 
        //thread and update the UI element on that thread
        dispatch_async(dispatch_get_main_queue(), {
            self.currentTemperatureLabel.text = result[0] //or whatever index you want
            return
        })
    })
}

Is this what your question was asking for?

Will M.
  • 1,864
  • 17
  • 28
  • thanks for taking the time to answer, I'll give this a shot this evening and see how it works. – Dan Beaulieu Apr 28 '15 at 17:25
  • Np, the only things you might need to change is what data type you pass in to completion, I don't know what "does some work creating objects from xml" does, but you can just change what the completion block takes in to match whatever you create. It can even have multiple parameters if you want to pass in multiple things. – Will M. Apr 28 '15 at 17:50
  • What is it that I'm supposed to pass IN to the completion handler? This method takes a url, requests and fetches XML data and parses it. The only thing I need passed in for the method to work is a url. – Dan Beaulieu Apr 28 '15 at 21:29
  • 2
    I guess `labelOne` is a ViewController `IBOutlet`; if this is the case, then since `labelOne` is used inside a closure you need to make explicit the reference to `self`, so you need to call `self.labelOne.text = ...`. Also if the ViewController owns `XMLUtility` (i.e. `retrieveDataFromXML` is not a class method) you create a reference cycle, so you may want `self` to be `unowned` inside the closure. Note also that if `retrieveDataFromXML` is not on the main queue, you cannot modify the UI directly, but you need to call the main queue back – user2340612 Apr 28 '15 at 22:50
  • 1
    @DanBeaulieu you pass the parsed XML in to the completion handler. The completion handler is a closure (block of code) that runs when you call completion(parameters), which is called after the xml is parsed. The parameters are whatever you need in order to do the things you wish done after the xml is parsed. In your case, you want the label text updated with something from the xml, so the completion handler needs to have the xml. – Will M. Apr 29 '15 at 14:06
  • @user2340612 you are correct and I will update my code to reflect that. – Will M. Apr 29 '15 at 14:07
  • I think maybe I need to do some refactoring in order to do this correctly... My original XML method never actually returned anything, It just assigned values to an array that was declared as a globally (which I know is bad practice). I'm going to modify my post with more code. – Dan Beaulieu Apr 29 '15 at 14:27
  • I've updated my post. But once again, I am sure that the global array is a bad way of doing things that's why I've started this bounty. So could you please provide an example where we pass [DayForecast] back to the controller? Sorry for the hassle! If I get this to work I'll award you the bounty immediately. – Dan Beaulieu Apr 29 '15 at 14:45
  • @DanBeaulieu First, the xml parsing code runs. Then, you call completion(weekForecasts) to pass the local array you were using to the completion block. Then the completion block runs, with that array, and updated the label in your viewcontroller. – Will M. Apr 29 '15 at 17:11
  • getting error: Cannot invoke 'dispatch_async' with an argument list of type '(dispatch_queue_t!, () -> _)' – Dan Beaulieu Apr 29 '15 at 17:47
  • Will, you're the man. Thank you for helping me with this. I understand how this all works now... well except for the dispatch_async, but I think I'll be able to figure that out. – Dan Beaulieu Apr 29 '15 at 17:53
  • Try adding a return to the end of the closure – Will M. Apr 29 '15 at 17:53
  • Do I just add a return type to the closure? Say "String" for instance? then include a return "derp"? I'm asking because this just threw errors. – Dan Beaulieu Apr 29 '15 at 18:03
  • Just return. I whipped up a test app to see how this all worked and I am not getting any issues with it, but its also a bit simpler than what you have (I deleted the network stuff). What errors did it throw? – Will M. Apr 29 '15 at 18:09
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/76570/discussion-between-will-m-and-dan-beaulieu). – Will M. Apr 29 '15 at 18:11