Web Requests in Swift

July 25, 2018

For one of the apps I developed I wanted to interface with an API hosted somewhere else on the web. This is the framework I set up to make basic web requests, and it consists of two main components and one optional one.

The Request Factory

The first handles creating request objects, and uses basic string manipulation along with the URLRequest object from Foundation.

class Http {

    public static func join(parameters: [String: String]) -> String {
        if parameters.isEmpty { return "" }
        var items = [String]()
        for (key, value) in parameters { items.append("\(key)=\(value)") }
        return items.joined(separator: "&")
    }

    /** Generate a POST request to a URL. */
    public static func post(url: String, parameters: [String: String]=[:]) -> URLRequest {
        var request = URLRequest(url: URL(string: url)!)
        request.httpMethod = "POST"
        request.httpBody = Http.join(parameters: parameters).data(using: .utf8)
        return request
    }

    /** Generate a GET request to a URL. */
    public static func get(url: String, parameters: [String: String]=[:]) -> URLRequest {
        let get = Http.join(parameters: parameters)
        var request = URLRequest(url: URL(string: url + "?" + get)!)
        request.httpMethod = "GET"
        return request
    }

}

The Request Manager

The next component actually handles executing these requests, and relies on the URLSession object. There’s a reset function for developer convenience; it closes all requests connected to the session and clears cached data. Depending on the data you’re reading, it might invoke the third component, decodeHtmlEntities, which converts things like & to &. I used an implementation of this from Stack Overflow, so I’ll leave that to the reader.

public class Requests {

    private static var session: URLSession = URLSession(configuration: .default)

    /** Reset the session. */
    public class func reset() {
        self.session.finishTasksAndInvalidate()
        self.session = URLSession(configuration: .default)
    }

    /** Make an HTTP request expecting a plaintext response. */
    public class func make(
        with request: URLRequest,
        resolve: @escaping (_ string: String, _ response: HTTPURLResponse) -> Void,
        reject: @escaping (_ error: URLError?) -> Void) {

        /* Make the data request. */
        self.session.dataTask(with: request, completionHandler: { data, response, error in
            if let response = response as? HTTPURLResponse, let data = data {
                var string = String(data: data, encoding: .utf8)!
                // string = decodeHtmlEntities(string)
                resolve(string, response)
            } else { reject(error) }
        }).resume()

    }

}

Examples and Extensions

Here’s an example of how to use this framework:

// Get https://my.api/endpoint?key=100
let request = Http.get(url: "https://my.api/endpoint", parameters: ["key": 100])
Requests.make(with: request, resolve: { page, response in
    print(page)  // Raw data from the page
}, reject: print)

Keep in mind that these requests are asynchronous, so it can be helpful to adopt a promise-like method of yielding values from an API call. For example, if we want to add a wrapper for parsing JSON, we can do the following:

func json(
    with request: URLRequest,
    resolve: @escaping (_ data: Any, _ response: HTTPURLResponse) -> Void,
    reject: @escaping (_ error?: URLError) -> Void) {

    Requests.make(with: request, resolve: { page, response in
        var json: Any
        let data = page.data(using: .utf8)!
        do { json = try JSONSerialization.jsonObject(with: data) }
        catch {
            reject()
            return
        }
        resolve(json, response)
    }, reject: reject)
}