How to use URLSession

I'm trying to learn SwiftUI by converting a shell script to a SwiftUI app. It involves parsing some XML data that comes from the web. I managed to get the parsing to work using sample data, but when I tried to add the final piece, actually grabbing the data from the web, I got stuck. I found some sample code that works by itself, but when I tried to put it into a function that would take a URL and return the data, it returns an empty string. I guess it's an async problem, but I can't find sample code that will work for me. This is an app that only I will use. Here is what I have.

func doHttpCall(st : String) -> String {
    var xmlData = ""

    let url = URL(string: st)!

    let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
        guard let data = data else { return }
        xmlData = String(data: data, encoding: .utf8)!
        print(xmlData)
    }

    task.resume()

    return xmlData
}

It does fetch the data, as it is printing it, but it's returning an empty string. Any help will be appreciated.

Accepted Reply

That’s because doHTTPCall(…) is a synchronous function but networking is inherently asynchronous. You’re calling dataTask(…), which is an asynchronous callback-based routine. It starts the network request and then returns, and later on it calls the closure with the results. In most cases doHTTPCall(…) has already returned by the time that happens.

There are two ways you can tackle this:

  • Rework doHTTPCall(…) to use the same approach as dataTask(…), that is, take a completion handler that it calls when it’s done.

  • Change doHTTPCall(…) to a Swift async function. That’ll allow it to call the data(for:delegate:) method, which is also an async function.

Given that you’re just starting out, I recommend that use the second approach.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Replies

That’s because doHTTPCall(…) is a synchronous function but networking is inherently asynchronous. You’re calling dataTask(…), which is an asynchronous callback-based routine. It starts the network request and then returns, and later on it calls the closure with the results. In most cases doHTTPCall(…) has already returned by the time that happens.

There are two ways you can tackle this:

  • Rework doHTTPCall(…) to use the same approach as dataTask(…), that is, take a completion handler that it calls when it’s done.

  • Change doHTTPCall(…) to a Swift async function. That’ll allow it to call the data(for:delegate:) method, which is also an async function.

Given that you’re just starting out, I recommend that use the second approach.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks for the reply. I had thought that parsing the xml would be hard part but it turned out to be the easiest part of this little app so far. By contrast, I've spent days just trying to figure out how to do what I thought would be a simple http request. I just can't find sample code to do what I want to do. I know that the problem is the network reply is coming back after the function returns, I just don't know how to tell it to wait. Based on your reply I've now come up with this:

func doHttpCall(st : String) async -> String {
    var xmlData = ""
    
    let url = URL(string: st)!
    let urlRequest = URLRequest(url: url)
    
    do {
        let (data, _) =  try await URLSession.shared.data(for: urlRequest)
        xmlData = String(data: data, encoding: .utf8)!
        print(xmlData)
    }
    catch  {
        print("error")
    }
    
    return xmlData
}

That gives me the error: 'async' call in a function that does not support concurrency.

Is there any chance you could point me to some sample code or app that does what I want? I find I learn best from seeing sample code rather that trying to read the documentation. Thanks again.

I think I finally found a workable solution. I kept searching and found some code that uses semaphores and incorporated it into my original function and it seems to work.

func doHttpCall(st : String) -> String {
    var xmlStr = ""

    let url = URL(string: st)!
    let semaphore = DispatchSemaphore(value: 0)
    let task =  URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in
                if let error = error {
                print("Error: \(error)")
            } else if let response = response as? HTTPURLResponse,
                300..<600 ~= response.statusCode {
                    print("Error: \(response.statusCode)")
            } else {
                let mydata = data!
                xmlStr = String(data: mydata, encoding: .utf8)!
                print(xmlStr)
            }
            semaphore.signal()
        })

    task.resume()
    _ = semaphore.wait(timeout: DispatchTime.distantFuture)
    return xmlStr
}

And I even understand what it's doing.

If you have a better solution, I'll always be happy to see it. But sending an http request and getting a response is just an incidental but necessary part of my little app. It only took me a week to find a solution. Thanks again for your trouble.

I think I finally found a workable solution.

That works, but it’s a serious anti-pattern. You are blocking a thread waiting for the network request to finish, which is either extremely bad (if it’s the main thread), horribly bad (if it’s a Swift concurrency pool thread), generally bad (if it’s a Dispatch worker thread), or probably bad (if it’s a thread you created).

The correct fix depends on the nature of the calling code. Looking upthread I noticed this:

I'm trying to learn SwiftUI by converting a shell script to a SwiftUI app.

which suggests that you’ll want to switch to an async main function. For example:

import Foundation

func main() async {
    do {
        print("did fetch")
        let url = URL(string: "https://example.com")!
        let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60.0)
        let (data, response) = try await URLSession.shared.data(for: request)
        let http = response as! HTTPURLResponse
        print("did fetch, status: \(http.statusCode), count: \(data.count)")
    } catch {
        print("did not fetch")
    }
}

await main()

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"