Swift – make your life easier with promises

A while ago I wrote a series of articles about Promises in Objective-C, and described the way promises help us write async code in an async manner, help us pipeline data streams and recover from errors by giving the pipeline another thing to process. Well, time has passed, Swift came along, and promises look better than ever, thanks to features of the programming language.

Note. The rest of the article makes use of the CKPromise.Swift library (https://github.com/cristik/CKPromise.Swift).

Let’s dive in, and begin with a simple example – sending a NSURLRequest over a NSURLSession, and parse the received data (we assume it’s a JSON) into a dictionary. To keep things clean we’ll add extensions over NSURLSession, respectively NSData for these two tasks. The code might look like this:

[swift gutter=”false”]
public extension NSURLSession {
func sendRequest(request: NSURLRequest) -> Promise<NSData,NSError> {
let promise = Promise<NSData,NSError>()
let task = self.dataTaskWithRequest(request) { data, urlResponse, error in
if let error = error {
// we have an error, means the request failed, reject promise
promise.reject(error)
} else if let data = data {
// we don’t have an error and we have data, resolve promise
promise.resolve(data)
} else {
// we have neither error, nor data, report a generic error
// another approach would have been to resolve the promise
// with an empty NSData object
promise.reject(NSError.genericError())
}
}
task.resume()
return promise
}
}

public extension NSData {
func parseJSON() -> Promise<[NSObject:AnyObject], NSError> {
let promise = Promise<[NSObject:AnyObject], NSError>()
if let parsedJSON = try? NSJSONSerialization.JSONObjectWithData(self, options: []),
let result = parsedJSON as? [NSObject:AnyObject] {
// yay, we were able to parse, and received a dictionary
promise.resolve(result)
} else {
// 🙁 report an invalid json error
promise.reject(NSError.invalidJSONError())
}
return promise
}
}

public extension NSError {
class func genericError() -> NSError {
return NSError(domain: "GenericErrorDomain", code: -1, userInfo: nil)
}

class func invalidJSONError() -> NSError {
return NSError(domain: "InvalidJSONErrorDomain", code: -1, userInfo: nil)
}
}
[/swift]

With the above methods available, the actual code looks something like this:

[swift gutter=”false”]
// Please ignore the forced unwrap for now
let url = NSURL(string: "https://jsonplaceholder.typicode.com/posts/1")!
let request = NSURLRequest(URL: url)
NSURLSession.sharedSession().sendRequest(request).onSuccess({
return $0.parseJSON()
}).onSuccess( {
print("Parsed JSON: \($0)")
}).onFailure( {
print("Failed with error: \($0)")
})
[/swift]

Let’s continue with something more useful. Now that we have the JSON of a post, let’s make use of it and create a model.

[swift gutter=”false”]
struct Post {
private(set) var id: Int = 0
private(set) var userId: Int = 0
private(set) var title: String = ""
private(set) var body: String = ""

static func fromDictionary(dictionary: [NSObject:AnyObject]) -> Promise<Post,NSError> {
let promise = ()
guard let id = dictionary["id"] as? Int,
userId = dictionary["userId"] as? Int else {
// the above two fields are mandatory, reject the promise if missing or invalid
return Promise<Post,NSError>.rejected(NSError.invalidDictionaryError())
}

var post = Post()
post.id = id
post.userId = userId
post.title = dictionary["title"] as? String ?? ""
post.body = dictionary["body"] as? String ?? ""

return Promise<Post,NSError>.fulfilled(post)
}
}
[/swift]

We declared a struct for the model, and we added support for creating a new model in a promised way. Why did we do this? Because it enables us the following flow:

[swift gutter=”false”]
NSURLSession.sharedSession().sendRequest(request).onSuccess({
return $0.parseJSON()
}).onSuccess( {
return Post.fromDictionary($0)
}).onSuccess({
print("Received post: \($0)")
}).onFailure( {
print("Failed with error: \($0)")
})
[/swift]

We added one more step to the pipeline in one of the most easiest way we could add it. The code is short and clean and transmits very well it’s intend.

We can go even further and consider the pipeline successful only when the post is saved into the local database for example:

[swift gutter=”false”]
NSURLSession.sharedSession().sendRequest(request).onSuccess({
return $0.parseJSON()
}).onSuccess( {
return Post.fromDictionary($0)
}).onSuccess( {
return databaseHelper.savePostToDB($0)
}).onSuccess({
print("Received and saved post: \($0)")
}).onFailure( {
print("Failed with error: \($0)")
})
[/swift]

There’s no limit of the length of the chain (pipeline).

We can also go the other way around, send a local post to the server:

[swift gutter=”false”]
databaseHelper.readPostWithID(18).onSuccess({
return $0.toJSONDictionary()
}).onSuccess({
let urlRequest = NSURLRequest(….)
// configure the request payload
return NSURLSession.sharedSession().sendRequest(urlRequest)
}).onSuccess({
print("Successfully sent the post to server")
}).onFailure({
print("Oh no, an error occurred: \($0)")
})
[/swift]

The above code snippets exemplify how promises help us write clean/short code, that helps with the separation of concerns and allow easy development of long processing pipelines, all while allowing asynchronous execution of the operations.

This is only the first part of the series of Swift promises. So stay tuned :).

P.S. As you might have noticed, the parseJSON() method of NSData is not an async one. The main thread is blocked while the json is parsed, so we’re not fully async there. Luckily this is easy to change, just dispatch the json parsing code onto another queue and resolve/reject from there:

[swift gutter=”false”]
func parseJSON() -> Promise<[NSObject:AnyObject], NSError> {
let promise = Promise<[NSObject:AnyObject], NSError>()
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
if let parsedJSON = try? NSJSONSerialization.JSONObjectWithData(self, options: []),
let result = parsedJSON as? [NSObject:AnyObject] {
// yay, we were able to parse, and received a dictionary
promise.resolve(result)
} else {
// 🙁 report an invalid json error
promise.reject(NSError.invalidJSONError())
}
});
return promise
}
[/swift]

Now, we’re fully async.

Leave a Reply

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