Introducing Kilo

Kilo is an open-source framework for consuming REST services in iOS or tvOS. It is extremely lightweight and provides a convenient, callback-based interface that makes it easy to interact with remote APIs.

For example, the following code snippet shows how a client application might access a simple service that returns a friendly greeting. The request is executed asynchronously, and the result is printed when the call returns:

webServiceProxy.invoke(.get, path: "/hello") { (result: String?, error: Error?) in
    if let greeting = result {
        print(greeting) // "Hello, World!"
    }
}

The project’s name comes from the nautical K or Kilo flag, which means “I wish to communicate with you”:

This article introduces the Kilo framework and provides an overview of its key features.

WebServiceProxy Class

Kilo is distributed as a universal binary that will run in the iOS simulator as well as on an actual device. The framework contains a single class named WebServiceProxy that is used to issue API requests to the server.

Service proxies are initialized via init(session:serverURL:), which takes the following arguments:

  • session – a URLSession instance that is used to create service requests
  • serverURL – the base URL of the service

A service operation is initiated via one of the following methods:

public func invoke(_ method: Method, path: String,
    arguments: [String: Any] = [:], content: Data? = nil, contentType: String? = nil,
    resultHandler: @escaping (_ result: T?, _ error: Error?) -> Void) -> URLSessionTask? { ... }

public func invoke(_ method: Method, path: String,
    arguments: [String: Any] = [:], content: Data? = nil, contentType: String? = nil,
    resultHandler: @escaping (_ result: T?, _ error: Error?) -> Void) -> URLSessionTask? { ... }

public func invoke(_ method: Method, path: String,
    arguments: [String: Any] = [:], content: Data? = nil, contentType: String? = nil,
    responseHandler: @escaping (_ content: Data, _ contentType: String?) throws -> T?,
    resultHandler: @escaping (_ result: T?, _ error: Error?) -> Void) -> URLSessionTask? { ... }

All three methods accept the following arguments:

  • method – the HTTP method to execute
  • path – the path to the requested resource
  • arguments – a dictionary containing the method arguments as key/value pairs
  • content – an optional Data instance representing the body of the request
  • contentType – an optional string value containing the MIME type of the content
  • resultHandler – a callback that will be invoked upon completion of the method

The first version of the method uses JSONSerialization to decode response data. The second uses JSONDecoder to return a decodable value. The third version accepts an additional responseHandler argument to facilitate decoding of custom response content (for example, a UIImage).

All three methods return an instance of URLSessionTask representing the invocation request. This allows an application to cancel a task, if necessary.

Arguments

Like HTML forms, arguments are submitted either via the query string or in the request body. Arguments for GET, PUT, PATCH, and DELETE requests are always sent in the query string.

POST arguments are typically sent in the request body, and may be submitted as either “application/x-www-form-urlencoded” or “multipart/form-data” (determined via the service proxy’s encoding property). However, if a custom body is specified via the content parameter, POST arguments will be sent in the query string.

Any value that provides a description property may be used as an argument. This property is generally used to convert the argument to its string representation. However, Date instances are automatically converted to a 64-bit integer value representing epoch time (the number of milliseconds that have elapsed since midnight on January 1, 1970).

Additionally, array instances represent multi-value parameters and behave similarly to tags in HTML. Further, when using the multi-part form data encoding, instances of URL represent file uploads and behave similarly to tags in HTML forms. Arrays of URL values operate similarly to tags.

Return Values

The result handler is called upon completion of the operation. If successful, the first argument will contain a deserialized representation of the content returned by the server, and the second argument will be nil. Otherwise, the first argument will be nil, and the second will be populated with an Error instance describing the problem that occurred.

Note that, while service requests are typically processed on a background thread, result handlers are always executed on the application’s main thread. This allows result handlers to update the user interface directly, rather than posting a separate update operation to the main queue.

If the server returns an error response, a localized description of the error will be provided in the localized description of the error parameter. Further, if the error is returned with a content type of “text/plain”, the response body will be returned in the error’s debug description.

Example

The following code snippet demonstrates how the WebServiceProxy class might be used to access the operations of a simple math service:

// Create service proxy
let webServiceProxy = WebServiceProxy(session: URLSession.shared, serverURL: URL(string: "http://localhost:8080")!)

// Get sum of "a" and "b"
webServiceProxy.invoke(.get, path: "/math/sum", arguments: [
    "a": 2,
    "b": 4
]) { (result: Int?, error: Error?) in
    // result is 6
}

// Get sum of all values
webServiceProxy.invoke(.get, path: "/math/sum", arguments: [
    "values": [1, 2, 3, 4]
]) { (result: Int?, error: Error?) in
    // result is 10
}

Additional Information

This article introduced the Kilo framework and provided an overview of its key features. For additional information, see the the project README.