Mislav Javor

Protocol Oriented Networking Layer In Swift

Protocol oriented programming

Protocol-oriented programming is a new approach to developing applications in Swift. It’s not a new paradigm, nor was it introduced by Swift. It’s simply a new name for a cool concept which can be very useful in abstracting away the complexites of our apps. It also allows us to avoid certain object-oriented pitfalls such as the fragile base class issue and god class issue when dealing with inheritance.

Networking layer with protocol-oriented programming

End result

The end result of this approach will be that we’ll be able to call our service by doing the following:

UserModel.fetch(Router.getUser(id: 1200), onSuccess { result in
    if case let .asSelf(model) = result {
        print(model.name)
    }
}, onError: { error in
    print(error)
})

Setup

I found the paradigm to be really useful in creating an expressive networking layer for my latest application

For this you’ll need:

The steps are:

Let’s assume our endpoints are

Fetchable protocol

First we’ll make a helper enum so we can support root arrays and dictionaries

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

The unboxer will try to deserialize the result in the same order that it was defined in the enum

We’ll define Fetchable as an empty protocol

protocol Fetchable {}

and implement the fetch(...) function in the protocol extension

In order to do that, we need to implement a protocol extension with a Self constraint. In this context - Self refers to a struct, class or enum implementing the Fetchable protocol.

We constrain Self to be Unboxable (Unboxable is a protocol in the Unbox library - you could’ve just as easily constrained Self to Mappable if you were using ObjectMapper or any other protocol).

So our protocol extension signature looks like this

extension Fetchable where Self: Unboxable { ... }

This means that when we use Self as a type in the extension - we can access all the methods that are defined in the Unboxable protocol

So let’s go and create the fetch(...) function. First off - we’ll define some helpful typealiases

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

These are used just for convenience

The fetch function will look like this

static func fetch(with request: URLRequestConvertible, onSuccess: @escaping SuccessHandler<Self>, onError: @escaping ErrorHandler){

        Alamofire.request(request).responseJSON { response in
            if let errorData = response.result.error {
                onError(errorData)
                return
            }
            if let data = response.data {
                do {
                    let mapped: Self = try unbox(data: data)
                    onSuccess(.asSelf(mapped))
                } catch {
                    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 {
                        do {
                            let mapped: [Self] = try unbox(data: data)
                            onSuccess(.asArray(mapped))
                        } catch {
                            do {
                                onSuccess(.raw(data))
                            } catch {
                                onError(error)
                            }
                        }
                    }
                }
            }
        }

    }

Now let’s define our models:

struct ListItemModel : Fetchable, Unboxable {
    let id: Int
    let name: String

    init(unboxer: Unboxer) throws {
        id = try unboxer.unbox(key: "id")
        name = try unboxer.unbox(key: "name")
    }
}

struct UserModel: Fetchable, Unboxable {
    let fistName: String
    let lastName: String

    init(unboxer: Unboxer) throws {
        firstName = try unboxer.unbox(key: "first_name")
        lastName = try unboxer.unbox(key: "last_name")
    }
}

struct ProductModel: Fetchable, Unboxable {
    let sku: String
    let weight: Int

    init(unboxer: Unboxer) throws {
        sku = try unboxer.unbox(key: "sku")
        weight = try unboxer.unbox(key: "weight")
    }
}

The Router is defined as a classic swift enum router. Plenty of other tutorials that cover that

And voilà you can now call the endpoints with the super-expressive

UserModel.fetch(Router.getUser(id: 150),
onSuccess: { result in

}, onError: { error in

})

And they are completely parsed and ready to use.

Mislav Javor

I'm an entrepreneur and a software developer. CEO of a blockchain startup aiming to simplify buying and selling of electricity. Actively participating in the proliferation of healthy (technology oriented) blockchain culture. Organizer of Blockchain Development Meetup Zagreb, lectured at HUB385 Academy and University of Osijek on topics of smart comtract development. In my free time, I'm a singer and a guitar/piano player. Contact at mislav@ampnet.io

Follow me on Twitter