How to use async/await with URLSession.shared.Data in iOS 15?

What is Async/await?

In the latest Swift, the new async/await API is introduced for iOS 15. The async/await aims at providing a new way to implement concurrent code, and it is especially handy when working with URLSession. In this post I will briefly go over how to use async/await and the new URLSession API to implement a call to web application.

The old way: URLSession.shared.DataTask

Previously I wrote about the old way of using URLSession.shared.DataTask to implement web application calls. Along the same line, let’s take a look at how we implement a simple call using the same convenience function:

//Convenience function introduced previously, so that we can hook any completion function with it
private func resumeDataTask(withRequest request: URLRequest, completion: @escaping (Result<Data, Error>) -> Void) {
        let task = URLSession.shared.dataTask(with: request, completionHandler: {(data, response, error) -> Void in
           if let httpResponse = response as? HTTPURLResponse {
                if httpResponse.statusCode != 200 {
                    if let error = error {
                        completion(.failure(error))
                        print(error)
                        return
                    }
                    else {
                        completion(.failure(NetworkError.badResponse))
                    }
                }
                else if let data = data {
                    completion(.success(data))
                }
                else if let error = error {
                    completion(.failure(error))
                }
                else {
                    print("Unknown case to be handled...")
                }
           }
            else {
                completion(.failure(NetworkError.timeout))
            }
       })       
       task.resume()
}

//A simple completion function
func urlCompletion(_ result:Result<Data, Error>) -> Void {
        switch result {
        case .failure(let error):
            DispatchQueue.main.async {
                //Do something in case of failure. Here we assume we change something on the UI by urlFetchSuccess
                self.urlFetchSuccess = false
            }
        case .success(_):
            DispatchQueue.main.async {
                //Do something in case of failure. Here we assume  we change something on the UI by urlFetchSuccess
                self.urlFetchSuccess = true
            }
        }
    } 

func fetchResult(string: url) {
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        resumeDataTask(withRequest: request, completion: urlCompletion)
}

The above shows how we use URLSession.shared.DataTask before async/await. Typically we create a convenience function that does the networking call, and create a request (like in fetchResult) and hook an appropriate completion function with the convenience function call that handles success or failure cases differently based on needs.

The new way: URLSession.shared.Data

Let’s see how async/await simplifies the code for us:

//Convenience function introduced previously, so that we can hook any completion function with it
private func resumeDataTask(withRequest request: URLRequest) async throws -> Data {
        do {
           let (data, response) = try await URLSession.shared.data(for: request)
           guard (response as? HTTPURLResponse)?.statusCode == 200 else {throw NetworkError.badResponse)}
           return data
        }
        catch {
           throw NetworkError.timeout
        } 
}

func fetchResult(url: URL) async throws -> Void {
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        do {
            let data = try await resumeDataTask(withRequest: request) //Note now we removed the completion handler
            //What happens in success case goes here
            self.urlFetchSuccess = true
        }
        catch NetworkError.badResponse, NetworkError.timeout { //What happens in failure case goes here
            self.urlFetchSuccess = false
        }
}

The code now looks a lot simpler. What happens here is by bringing in async/await, the code becomes very sequential. When calling an async function, we must use the keyword “await”, and it simply returns the expected results (in our case, the data). Errors are thrown if we declare the function “async throws”, and in this case we use “try await”.

As we can see, we are now using do…catch to handle the results. With the new definition, it is either successful, therefore we handle the data after the try await call, or it is an exception that is handled by catch.

The new URLSession.shared.Data is an async version of the old URLSession.shared.DataTask. The system knows to transfer the control back to our code after the data call is done, and continues the execution of next line, in an asynchronous way. So we not longer need to provide a completion handler. What we now do is to write what’s supposed to happen in the completion handler just in the next line, or as exception handler.

Actually, now that the URLSession.shared.Data seems a lot easier to use, it doesn’t hurt that we write the handling in one function and just get rid of our convenience function:

func fetchResult(string: url) async -> Void {
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        do {
           let (data, response) = try await URLSession.shared.data(for: request)
           guard (response as? HTTPURLResponse)?.statusCode == 200 else {throw NetworkError.badResponse)}
           //What happens in success case goes here, including possibly handling the data
           self.urlFetchSuccess = true
        }
        catch {
           //For this simple example, we can just do the failure handling here
           self.urlFetchSuccess = false
        } 
}

Leave a ReplyCancel reply