1

I am trying to recover a data set from a URL (after parsing a JSON through the parseJSON function which works correctly - I'm not attaching it in the snippet below).

The outcome returns nil - I believe it's because the closure in retrieveData function is processed asynchronously. I can't manage to have the outcome saved into targetData.

Thanks in advance for your help.

class MyClass {
    
    var targetData:Download?
    
    func triggerEvaluation() {
        retrieveData(url: "myurl.com") { downloadedData in
            self.targetData = downloadedData
        }
        print(targetData) // <---- Here is where I get "nil"!
    }
    
    func retrieveData(url: String, completion: @escaping (Download) -> ()) {
        let myURL = URL(url)!
        let mySession = URLSession(configuration: .default)
        let task = mySession.dataTask(with: myURL) { [self] (data, response, error) in
            if error == nil {
                if let fetchedData = data {
                    let safeData = parseJSON(data: fetchedData)
                    completion(safeData)
                }
            } else {
                //
            }
        }
        task.resume()
    }
}
gcharita
  • 7,729
  • 3
  • 20
  • 37
MRoot
  • 45
  • 6
  • 1
    Thanks mate - Should I use the DispatchQueue.main.async inside the closure? I.e.: func triggerEvaluation() { retrieveData(url: "myurl.com") { downloadedData in Dispatchqueue.main.async { self.targetData = downloadedData print(targetData) } }} – MRoot Nov 14 '20 at 23:34
  • And also: how can I pass my populated "targetData" variable to a view? Should I adopt a protocol-delegate structure? I'm asking as if I just create an instance of this class and access to the property from the view, it results to be nil (suppose for the same reason). Thanks! – MRoot Nov 14 '20 at 23:47
  • “how can I pass my populated "targetData" variable to a view?” ... usually you’d just supply the data to the view, resulting in the necessary UI controls to be updated. No protocol-delegate is strictly needed, but that pattern might be very useful if the view allows user input, and you need to reflect user input data back in'to the model, the web service, etc. – Rob Nov 15 '20 at 06:58

1 Answers1

0

Yes, it’s nil because retrieveData runs asynchronously, i.e. the data hasn’t been retrieved by the time you hit the print statement. Move the print statement (and, presumably, all of the updating of your UI) inside the closure, right where you set self.targetData).

E.g.

func retrieveData(from urlString: String, completion: @escaping (Result<Download, Error>) -> Void) {
    let url = URL(urlString)!
    let mySession = URLSession.shared
    let task = mySession.dataTask(with: url) { [self] data, response, error in
        guard 
            let responseData = data,
            error == nil, 
            let httpResponse = response as? HTTPURLResponse,
            200 ..< 300 ~= httpResponse.statusCode
        else {
            DispatchQueue.main.async {
                completion(.failure(error ?? NetworkError.unknown(response, data))
            }
            return
        }

        let safeData = parseJSON(data: responseData)
        DispatchQueue.main.async {
            completion(.success(safeData))
        }
    }
    task.resume()
}

Where

enum NetworkError: Error {
    case unknown(URLResponse?, Data?)
}

Then the caller would:

func triggerEvaluation() {
    retrieveData(from: "https://myurl.com") { result in
        switch result {
        case .failure(let error):
            print(error)
            // handle error here

        case .success(let download):
            self.targetData = download
            // update the UI here
            print(download)
        }
    }
    // but not here
}

A few unrelated observations:

  • You don't want to create a new URLSession for every request. Create only one and use it for all requests, or just use shared like I did above.

  • Make sure every path of execution in retrieveData calls the closure. It might not be critical yet, but when we write asynchronous code, we always want to make sure that we call the closure.

  • To detect errors, I'd suggest the Result pattern, shown above, where it is .success or .failure, but either way you know the closure will be called.

  • Make sure that model updates and UI updates happen on the main queue. Often, we would have retrieveData dispatch the calling of the closure to the main queue, that way the caller is not encumbered with that. (E.g. this is what libraries like Alamofire do.)

Rob
  • 415,655
  • 72
  • 787
  • 1,044