Search code examples
jsonswifturlsession

How do I get the response from a URLSession before the rest of my code executes?


I have a problem with my URLSession in Swift. No matter what I try (or how many tutorials I read through), I cannot find a way to get the network request to finish before executing the rest of my code. I was under the impression that a completion handler would do the trick, but this has not proven to be the case. What am I doing wrong here?

struct DataManager {
    
    let eventsURL = URL(string: "https://spreadsheets.google.com/feeds/list/blablabla/1/public/full?alt=json")!
    let decoder = JSONDecoder()
    var data = Data()
    
    func downloadEvents() -> [Event] {
        var events: [Event] = []
        var downloadedEvents: [[String: Any]] = []
        
        getDataFromServer(forURL: eventsURL) { result in
            downloadedEvents = result
        }
        
        // This next part always executes before the response is receive, which means the downloadedEvents variable is always empty.

        for downloadedEvent in downloadedEvents {
            if let event = Event(fromJSON: downloadedEvent) {
                events.append(event)
            }
        }
        
        return events
    }
    
    func getDataFromServer(forURL url: URL, completion: @escaping (_ result: [[String: Any]]) -> Void) {
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let jsonData = data {
                let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: [])
                if let dictionary = jsonObject as? [String: Any] {
                    if let feed = dictionary["feed"] as? [String: Any] {
                        if let entry = feed["entry"] as? [[String: Any]] {
                            DispatchQueue.main.async {
                                completion(entry)
                            }
                        }
                    }
                }
            }
        }.resume()
    }
}

Solution

  • Your downloadEvents function need to be asynchronous because its calls another asynchronous function getDataFromServer.

    The solution may be to move the part for calculating Events to the asynchronous block and provide a completion parameter:

    func downloadEvents(completion: @escaping ([Event]) -> ()) {
        var events: [Event] = []
        var downloadedEvents: [[String: Any]] = []
    
        getDataFromServer(forURL: eventsURL) { result in
            downloadedEvents = result
            for downloadedEvent in downloadedEvents {
                if let event = Event(fromJSON: downloadedEvent) {
                    events.append(event)
                }
            }
            completion(events)
        }
    }
    

    Note

    Your code may actually be simplified to:

    func downloadEvents(completion: @escaping ([Event]) -> ()) {
        getDataFromServer(forURL: eventsURL) { completion($0.compactMap(Event.init(fromJSON:))) }
    }