Skip to content

Instantly share code, notes, and snippets.

@AppleBetas
Last active August 26, 2022 11:41
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save AppleBetas/75103b8e3c1bec30187657ddc7db4d4a to your computer and use it in GitHub Desktop.
Save AppleBetas/75103b8e3c1bec30187657ddc7db4d4a to your computer and use it in GitHub Desktop.
API.swift - A dead simple drop-in URLRequest wrapper for making easy API requests in Swift.
//
// API.swift
//
// Created by AppleBetas on 2017-01-15.
// Copyright © 2017 AppleBetas. All rights reserved.
//
import Foundation
class API {
static let shared = API(baseURL: URL(string: "YOUR_BASE_API_URL_HERE")!)
var baseURL: URL
static let isInDebugMode = true
/// Headers to add to every request, on top of any passed in the function parameters.
var globalHeaders = [String: String]()
/// Create an API object with the specified API base URL.
init(baseURL: URL) {
self.baseURL = baseURL
}
enum HTTPBody {
case text(String), data(Data), parameters([String: Any], ParameterEncoding)
/// The data to send after processing this HTTP body.
var data: Data? {
switch self {
case .text(let body):
return body.data(using: .utf8)
case .data(let body):
return body
case .parameters(let dict, let encoding):
return encoding.encode(body: dict)
}
}
/// Optionally, the content type to send with the body.
var contentType: String? {
switch self {
case .parameters(_, let encoding):
return encoding.contentType
case .text(_):
return "text/plain; charset=utf-8"
default:
return nil
}
}
}
enum HTTPMethod {
case GET, DELETE, OPTIONS
case POST(HTTPBody), PUT(HTTPBody), PATCH(HTTPBody)
/// The description of this method, also used by URLRequest.
var description: String {
switch self {
case .GET:
return "GET"
case .POST(_):
return "POST"
case .PUT(_):
return "PUT"
case .PATCH(_):
return "PATCH"
case .DELETE:
return "DELETE"
case .OPTIONS:
return "OPTIONS"
}
}
/// Optionally, a body to add to the request.
var body: Data? {
switch self {
case .POST(let body), .PUT(let body), .PATCH(let body):
return body.data
default:
return nil
}
}
/// Optionally, the content type to send with the body.
var contentType: String? {
switch self {
case .POST(let body), .PUT(let body), .PATCH(let body):
return body.contentType
default:
return nil
}
}
}
enum ParameterEncoding {
case formURL, json
/// Encode the provided body dictionary to Data.
func encode(body: [String: Any]) -> Data? {
switch self {
case .formURL:
return APIUtilities.formURLParameterEncoded(body: body).data(using: .utf8)
case .json:
return try? JSONSerialization.data(withJSONObject: body)
}
}
/// The default content type to request for this encoding.
var contentType: String? {
switch self {
case .formURL:
return "application/x-www-form-urlencoded; charset=utf-8"
case .json:
return "application/json; charset=utf-8"
}
}
}
/// Construct a URL from the provided endpoint and URL parameters.
private func getURL(with endpoint: String, urlParameters: [String: String?]? = nil) -> URL? {
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else { return nil }
components.path += endpoint
if let params = urlParameters {
components.queryItems = params.map { (k, v) in
return URLQueryItem(name: k, value: v)
}
}
return components.url
}
/**
Make a request with the provided parameters and return its data as a dictionary, after being decoded from JSON.
- parameter endpoint: The endpoint to request (appended to the base URL specified at init).
- parameter method: The HTTP method to use (and any data that might be included with this method's body). (defaults to GET)
- parameter urlParameters: The URL parameters to use as a query string appended to your URL.
- parameter headers: Any additional headers to include with the request. (defaults to none)
- parameter completionHandler: A callback accepting two optional parameters of the types [String: AnyObject] (being the received & decoded response) and Error to call upon completion.
*/
func makeRequest(to endpoint: String, method: HTTPMethod = .GET, urlParameters: [String: String?]? = nil, headers: [String: String] = [:], completionHandler: @escaping ([String: AnyObject]?, Error?) -> Void) -> URLSessionDataTask? {
guard let url = getURL(with: endpoint, urlParameters: urlParameters) else { completionHandler(nil, nil); return nil }
var headers = headers
globalHeaders.forEach { (k,v) in headers[k] = v }
return API.makeRequest(to: url, method: method, headers: headers, completionHandler: completionHandler)
}
/**
Make a request with the provided parameters and return its data.
- parameter endpoint: The endpoint to request (appended to the base URL specified at init).
- parameter method: The HTTP method to use (and any data that might be included with this method's body). (defaults to GET)
- parameter urlParameters: The URL parameters to use as a query string appended to your URL.
- parameter headers: Any additional headers to include with the request. (defaults to none)
- parameter completionHandler: A callback accepting two optional parameters of the types Data (being the received response) and Error to call upon completion.
*/
func makeRawRequest(to endpoint: String, method: HTTPMethod = .GET, urlParameters: [String: String?]? = nil, headers: [String: String] = [:], completionHandler: @escaping (Data?, Error?) -> Void) -> URLSessionDataTask? {
guard let url = getURL(with: endpoint, urlParameters: urlParameters) else { completionHandler(nil, nil); return nil }
var headers = headers
globalHeaders.forEach { (k,v) in headers[k] = v }
return API.makeRawRequest(to: url, method: method, headers: headers, completionHandler: completionHandler)
}
/**
Make a request with the provided parameters and return its data as a dictionary, after being decoded from JSON.
- parameter url: The URL to request.
- parameter method: The HTTP method to use (and any data that might be included with this method's body). (defaults to GET)
- parameter headers: Any additional headers to include with the request. (defaults to none)
- parameter completionHandler: A callback accepting two optional parameters of the types [String: AnyObject] (being the received & decoded response) and Error to call upon completion.
*/
static func makeRequest(to url: URL, method: HTTPMethod = .GET, headers: [String: String] = [:], completionHandler: @escaping ([String: AnyObject]?, Error?) -> Void) -> URLSessionDataTask {
return API.makeRawRequest(to: url, method: method, headers: headers) { data, error in
guard let data = data else { completionHandler(nil, error); return }
completionHandler((try? JSONSerialization.jsonObject(with: data, options: .allowFragments)) as? [String: AnyObject], error)
}
}
/**
Make a request with the provided parameters and return its data.
- parameter url: The URL to request.
- parameter method: The HTTP method to use (and any data that might be included with this method's body). (defaults to GET)
- parameter headers: Any additional headers to include with the request. (defaults to none)
- parameter completionHandler: A callback accepting two optional parameters of the types Data (being the received response) and Error to call upon completion.
*/
static func makeRawRequest(to url: URL, method: HTTPMethod = .GET, headers: [String: String] = [:], completionHandler: @escaping (Data?, Error?) -> Void) -> URLSessionDataTask {
// Construct the request
if isInDebugMode { print("Making \(method) request to \(url.absoluteString)") }
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 10)
request.httpMethod = method.description
request.httpBody = method.body
request.addValue(API.userAgentString, forHTTPHeaderField: "User-Agent")
if let contentType = method.contentType {
request.addValue(contentType, forHTTPHeaderField: "Content-Type")
}
for (name, value) in headers {
request.addValue(value, forHTTPHeaderField: name)
}
// Do the request!
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error -> Void in
completionHandler(data, error)
})
task.resume()
return task
}
/// A dynamically-generated user agent string to use in case one isn't specified (AppName/AppVersion).
static var userAgentString: String {
var userAgentString = "App"
if let bundleDisplayName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String {
userAgentString = bundleDisplayName
} else if let bundleName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String {
userAgentString = bundleName
}
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String {
userAgentString += "/\(version)"
}
return userAgentString
}
}
struct APIUtilities {
/// Encode the provided body using form URL parameter encoding and return it as a string.
static func formURLParameterEncoded(body: [String: Any]) -> String {
var result = [String]()
for (name, value) in body {
if let nameEncoded = name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let valueEncoded = String(describing: value).addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
result.append("\(nameEncoded)=\(valueEncoded)")
}
}
let resultStr = result.joined(separator: "&")
return resultStr
}
}
@AppleBetas
Copy link
Author

AppleBetas commented Jan 22, 2017

Some sample code:

Make a POST request, URL-encoded body:

API.shared.makeRequest(to: "login", method: .POST(.encoded([
    "username": username,
    "password": password
], .formURL))) { response, error in
    if let response = response {
        // response = dictionary of JSON body
    }
}

Make a GET request:

API.shared.makeRequest(to: "version") { response, error in
    if let response = response {
        // response = dictionary of JSON body
    }
}

updated to work with latest version

@AppleBetas
Copy link
Author

AppleBetas commented Jan 25, 2017

Added auto-setting user agent, support for headers, fixed PUT requests, moved parameter encoding into separate struct, and made it so that content-types are appropriately set based on type of content.

@EricRabil
Copy link

i love it

@EricRabil
Copy link

goals af

@EricRabil
Copy link

daddy

@AppleBetas
Copy link
Author

Added support for non-JSON requests, query parameters, as well as documented most of the methods, and slightly refactored.

@AppleBetas
Copy link
Author

Added support for getting the data task later on

@AppleBetas
Copy link
Author

Added support for PATCH requests, providing Data rather than String/Dictionaries for PUT, PATCH, and POST, making raw requests from inside an API object (to receive data back without decoding), a debug mode to log each request, and better URL generation code

@AppleBetas
Copy link
Author

Added better body data choosing (now an additional enum, rather than many enums for each HTTP request type). Also fixes default content type for PATCH text requests.

@AppleBetas
Copy link
Author

.formURLEncoded has been renamed to .formURL

@nekotiajones
Copy link

nekotiajones commented Nov 2, 2017

Great stuff! An error Type 'API.HTTPBody' has no member 'encoded' shows in Xcode when attempting your POST sample code. You can remove the .encoded and replace with .parameters and supply the parameters instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment