Networking in Swift part 1: URLSession and Async/await

Project code:

Level:

Easy

In Swift, there are many ways to make network requests to our API, the most popular are using third-party libraries like Alamofire or using the native URLSession system.

In this post we are going to see an example of an http client using the Swift URLSession class and the new async/await concurrency system.

The protocol

We start by creating the protocol to define the methods that our http client will have, we are going to call it HTTPClientType:

protocol HTTPClientType {
    func send(request: URLRequest) async throws -> Data?
}

We create a new function called send that receives as a parameter the request that we want to send and returns the result of the request in Data format.

The implementation

Let’s create a class to implement the protocol.

final class HTTPClient: NSObject, HTTPClientType {
    private var session: URLSession?

    init(session: URLSession? = nil) {
        self.session = session
        super.init()
    }

    func send(request: URLRequest) async throws -> Data? {
        // TODO:
    }
}

The constructor receives an instance of URLSession to be able to create the unit tests, but it is optional to be able to create a generic instance inside the class.

We create an extension of the class to create the private method that generates the URLSession in case it is nil.

extension HTTPClient {
    private func getURLSession() -> URLSession {
        guard let session = self.session else {
            let configuration = URLSessionConfiguration.default
            
            configuration.timeoutIntervalForRequest = 30.0
            configuration.timeoutIntervalForResource = 30.0
            
            let session = URLSession(configuration: configuration,
                                     delegate: nil,
                                     delegateQueue: nil)
            self.session = session
            return session
        }
        
        return session
    }
}

This function allows us to add the URLSessionDelegate in the future in case we want to add pinning for example.

The URLSession class has the data async method, but only on iOS 15 or higher. To be able to use this client in iOS 13 for example, we created two private methods, one for iOS 15 or higher and another for iOS below iOS 15:

private func makeRequestUnderiOS15(request: URLRequest) async throws -> (Data?, URLResponse?) {
    return try await withCheckedThrowingContinuation({ continuation in
        let dataTask = self.getURLSession().dataTask(with: request) { data, response, error in
            guard let error = error else {
                continuation.resume(returning: (data, response))
                return
            }
            continuation.resume(throwing: error)
        }

        dataTask.resume()
    })
}

@available(iOS 15.0, *)
private func makeRequest(request: URLRequest) async throws -> (Data?, URLResponse?) {
    return try await self.getURLSession().data(for: request)
}

With the two implementations of the call depending on the iOS version, we can now implement the send method:

func send(request: URLRequest) async throws -> Data? {
    var result: (data: Data?, response: URLResponse?)
    do {
        print("\(getName()): REQUEST \(request.httpMethod ?? "?") - \(request.url?.absoluteString ?? "?")")
        if #available(iOS 15.0, *) {
            result = try await self.makeRequest(request: request)
        } else {
            result = try await self.makeRequestUnderiOS15(request: request)
        }
    } catch {
        try self.handleRequestException(error)
    }

    let status = (result.response as? HTTPURLResponse)?.statusCode ?? 0

    if let httpResponse = result.response as? HTTPURLResponse,
       let date = httpResponse.value(forHTTPHeaderField: "Date") {
        print("\(getName()): RESPONSE \(status) - DATE: \(date)")
    }

    if let data = result.data {
        let body = String(decoding: data, as: UTF8.self)
        print("\(getName()): RESPONSE \(status) - BODY: \(body)")
    }

    guard let httpResponse = result.response as? HTTPURLResponse,
          (200..<300) ~= httpResponse.statusCode else {
        throw exceptionFromStatusCode(status)
    }

    return result.data
}

With this method we send the request and show the result of the request with the print function. Now we have to implement the possible errors, in the handleRequestException and exceptionFromStatusCode functions.

private func handleRequestException(_ error: Error?) throws {
    if let error = error as NSError?, error.domain == NSURLErrorDomain {
        if error.code == NSURLErrorNotConnectedToInternet {
            throw HTTPError.noInternet
        } else if error.code == NSURLErrorTimedOut {
            throw HTTPError.timeout
        }
    }
    throw HTTPError.clientError
}

private func exceptionFromStatusCode(_ status: Int) -> Error {
    switch status {
    case 401, 403:
        // Auth error
        return HTTPError.authenticationError
    case 404:
        return HTTPError.notFound
    case 409:
        return HTTPError.conflict
    default:
        // Server error
        return HTTPError.serverError
    }
}

Finally in the ViewController we are going to try to make a request:

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    Task {
        let client = HTTPClient()
        let request = URLRequest(url: URL(string: "https://dummyjson.com/products")!)
        let result = try await client.send(request: request)
        print("Result - \(result)")
    }
}

If you want you can check the project code in the following github repository.

In the next post we are going to add unit testing to this class.

Thank you for reading this post!

Leave a Reply

Your email address will not be published. Required fields are marked *