Protocol Oriented Networking example project

Ive created a simple demo app that demonstrates a protocol oriented programming approach for a networking stack using Swift on iOS. Its available here. Part of the app, the Router enum, is described in another article, available here.

I particularly like this networking solution because the end result is the ability to perform a network call from a data model object, with the results of the call packaged into objects of the type we used, if its possible. It will look like this :

UserData.call(with: urlRequest, onSuccess: { result in
    //handle call success
}, onError: { error in
    //handle call failure (call went - but server returned an error)
})

Breaking that down, UserData is my model struct, containing a few simple String properties. Ive made UserData conform to a protocol Ive created called Networkable. By doing this, I can use the function ‘call(..’ on it, and ‘call(..’ contains all of my networking functionality. The URLRequest is passed in, along with success and failure closures to handle the result of the call. Its all on one nice, concise line of code, and it’s really easy to read.

Ive used two libraries in the demo app – Alamofire and Unbox. Alamofire is a popular networking solution and Unbox does JSON unpacking. They are not really important for our protocol oriented approach – they just save some coding time. They are installed via Cocoapods.

I like to think of the process of making an object conform to a protocol, as like giving it a new skill, or modding it, or pimping it up. The approach in this case is to give our relatively dull data model UserData two additional skills : Networkable, and Unboxable.

struct UserData : Networkable, Unboxable {

Networkable is defined here.

You can see in the definition of Networkable that if the type using this protocol also conforms to Unboxable, then the ‘call(..’ func is available for it to use. That is encapsulated in the extension definition here :

extension Networkable where Self: Unboxable {
    static func call(..

The rest of the ‘call(..’ function is concerned with unpacking our data and attempting to make the kind of objects that we want, specified in the call itself (ie – we used ‘UserData.call(..’, indicating that we wish to try and create UserData type objects from any JSON we downloaded.

Theres a variety of ways that the JSON could have those objects packed up. There could be a single set of data that would directly populate a ‘UserData’ object, right at the root of the JSON. Or the JSON could contain a Dictionary of ‘UserData’ objects, or alternatively an Array. So this ‘call(..’ function tries each type one after the other to see what works. First we try to make a single object from the root of the JSON :

if let data = response.data {
    do {
        let mapped: Self = try unbox(data: data)
        onSuccess(.asSelf(mapped))
    } catch {
       {

If that doesn’t work, we ‘catch’ that error and go directly into the next thing, an attempt at making a ‘Dictionary’ of ‘UserData’ objects :

do {
    let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: [String: Any]]
    var mappedDictionary = [String: Self]()
    try json?.forEach { key, value in
        let data: Self = try unbox(dictionary: value)
        mappedDictionary[key] = data
    }
    onSuccess(.asDictionary(mappedDictionary))
} catch {
    {

When we call ‘onSuccess’ each time, there we specify exactly how we were successful using an enum called ‘MappingResult’ which is specified at the top of the ‘Networking’ swift file. It has four cases : asSelf, asDictionary, asArray, asRaw. When we create our success closure and pass it into this ‘call(..’ method, we handle each of those potential cases – we will of course only have one possible at a time.

The final thing we try and do is to create an ‘Array’, if we’ve failed at a single object and a ‘Dictionary’. The attempt to create one looks like this :

do {
    let mapped: [Self] = try unbox(data: data)
    onSuccess(.asArray(mapped))
} catch {
    onSuccess(.raw(data))
}

Finally you can see above that if the Array making part failed, we pass the received back as raw data.

Its worth having a closer look at how the ‘onSuccess part and the MappingResult enum work. The full definition for the ‘call(..’ function is like this :

static func call(with request: URLRequestConvertible, onSuccess: @escaping SuccessHandler, onError: @escaping ErrorHandler) {

First, @escaping allows the closure that is about to be defined to exist in memory beyond the lifetime of the calling object. SuccessHandler is a TypeAlias, meaning it is only there because the original definition looked complicated, and we wanted to swap it for something easier to read. What SuccessHandler really means is defined further up the page :

typealias SuccessHandler<T> = (MappingResult<T>) -> Void where T: Unboxable

..which is clearly a bit wordy and thats why we created the TypeAlias in the first place. The easiest way of explaining what the part after the ‘=’ does, is to show what it becomes when we actually use this function :

{ result in
    //handle call success
}

So the whole ‘() -> Void’ bit allows us to pass in a closure – the curly braces with code in-between. The ‘result in’ part there happens because we also had a type inbetween the brackets in the original definition, ‘MappingResult‘. This is our enum. The ‘‘ part is in the enum definition – we can pass various types along with the enum value, and the type we pass can be defined at runtime, but it must conform to ‘Unboxable’. So ‘result in’ will actual comprise two things at run-time: an enum value, and a ‘value bound’ property alongside it. Heres those possibilities from the enum definition :

enum MappingResult<T> {
    case asSelf(T)
    case asDictionary([String: T])
    case asArray([T])
    case raw(Data)
}

Those value-bound properties for each case are of the kind described by the case name : the ‘asDictionary’ case has a ‘Dictionary’ property bound to it, the ‘asArray’ case has an ‘Array’ bound, etc.

So in our case further above where we found an object of type ‘UserData’ at the root of the JSON, we pass back an enum of type MappingResult, case .asSelf, and our actual UserData object is passed as the property of that enum case.

What’s great about this system is that we can inform the code of the outcome of the call, we can advise it about the kind of data we received back, and we can pass that data back in the correct format, all in one go.

Heres how the calls looks, using a switch statement to handle all those outcomes :

UserData.call(with: urlRequest, onSuccess: { result in
    switch result {
    case .asSelf(let userData):
        userData.printContents()
    case .asDictionary(let userDataDict):
        print("Dictionary of UserData's : \(userDataDict)")
    case .asArray(let userDataArray):
        print("Array of UserData's : \(userDataArray)")
    case .raw(let rawData):
        print("Raw Data back : \(rawData)")
    }
}, onError: { error in
    print("Failed the test call, \(error.localizedDescription)")
})

Leave a Reply

Your e-mail address will not be published. Required fields are marked *