Teeps

Protocol-Oriented Networking in Swift

27 February 2017 by Chayel Heinsen

After developing a number of applications, we noticed that everyone's networking code was different. Every time maintenance developers had to take over a project, they had to "learn" the individual nuances of each network layer. This lead to a lot of wasted time. To solve this, our iOS Engineers decided to use the same networking setup. Thus we looked into Protocol-Oriented Networking.

Dependencies

To begin, we decided to build on Alamofire instead of URLSession. We did this for a few reasons: Alamofire is asynchronous by nature, has session management, reduces boilerplate code, and is very easy to use.

Let's get started

Endpoint

We start by creating a protocol called Endpoint. This will be used to define all of our API endpoints.

import Alamofire

protocol Endpoint {
    var baseURL: String { get } // https://example.com
    var path: String { get } // /users/
    var fullURL: String { get } // This will automatically be set. https://example.com/users/
    var method: HTTPMethod { get } // .get
    var encoding: ParameterEncoding { get } // URLEncoding.default
    var body: Parameters { get } // ["foo" : "bar"]
    var headers: HTTPHeaders { get } // ["Authorization" : "Bearer SOME_TOKEN"]
}

To make things even simpler, we create an extension to make some defaults.

extension Endpoint {
    // The encoding's are set up so that all GET requests parameters
    // will default to be url encoded and everything else to be json encoded
    var encoding: ParameterEncoding {
        return method == .get ? URLEncoding.default : JSONEncoding.default
    }

    // Should always be the same no matter what
    var fullURL: String {
        return baseURL + path
    }

    // A lot of requests don't require parameters
    // so we just set them to be empty
    var body: Parameters {
        return Parameters()
    }
}

Now that we have the protocol and extension in place, we can move on to building an endpoint. We start by creating an enum with all of our cases. Keep in mind you don't necessarily need to use an enum, we are using it here to group the same type of endpoints together. Also, it’s good to know that Endpoint is a protocol, which means you can use it with a class or struct.

enum UserEndpoints {
    case getUsers
    case getUser(id: String)
    case createUser(firstName: String, lastName: String)
}

Now that we know what UserEndpoints should do, let's conform to Endpoint.Since we are using an enum, we will switch on self.

extension UserEndpoints: Endpoint {
    // Set up the paths
    var path: String {
        switch self {
        case .getUsers: return "users/"
        case .getUser(let id): return "users/\(id)/"
        case .createUser: return "users/"
        }
    }

    // Set up the methods
    var method: HTTPMethod {
        switch self {
        case .getUsers: return .get
        case .getUser: return .get
        case .createUser: return .post
        }
    }

    // Set up any headers you may have. You can also create an extension on `Endpoint` to set these globally.
    var headers: HTTPHeaders {
        return ["Authorization" : "Bearer SOME_TOKEN"]
    }

    // Lastly, we set the body. Here, the only route that requires parameters is create.
    var body: Parameters {
        var body: Parameters = Parameters()

        switch self {
        case .createUser(let firstName, let lastName):
            body["first_name"] = firstName
            body["last_name"] = lastName
        default:
            break
        }

        return body
    }
}
Requests

Now that we have our endpoints, we need a way to use them!

We start by creating a protocol, BuckoErrorHandler, to globally handle errors. This is good for generic errors such as when requests time out.

We then create the Bucko struct to handle our requests. Here we set up the session manager, a singleton for Bucko and the delegate.

import Alamofire

protocol BuckoErrorHandler {
    func buckoRequest(request: URLRequest, error: Error)
}

typealias ResponseClosure = ((DataResponse<Any>) -> Void)

struct Bucko {
    // You can set this to a var if you want
    // to be able to create your own SessionManager
    let manager: SessionManager = SessionManager()
    static let shared = Bucko()
    var delegate: BuckoErrorHandler?
}

Now, create the method, request(endpoint:completion:) that takes an Endpoint and a closure to handle the response. In the response closure, you can see that we check for isSuccess or isFailure. If we have a failure, we notify our delegate. At this point the delegate can show some UI to handle the error. The error is also given to the completion closure if the callee wants to handle it as well.

extension Bucko {
    func request(endpoint: Endpoint, completion: @escaping ResponseClosure) -> Request {
        let request = manager.request(
            endpoint.fullURL,
            method: endpoint.method,
            parameters: endpoint.body,
            encoding: endpoint.encoding,
            headers: endpoint.headers
        ).responseJSON { response in

            if response.result.isSuccess {
                debugPrint(response.result.description)
            } else {
                debugPrint(response.result.error ?? "Error")
                // Can globably handle errors here if you want
                if let urlRequest = response.request, let error = response.result.error {
                    self.delegate?.buckoRequest(request: urlRequest, error: error)
                }
            }

            completion(response)
        }

        print(request.description)
        return request
    }
}
Conclusion

When it all comes together, your network calls look very small.

    let request = Bucko.shared.request(.getUser(id: "1")) { response in
        if response.result.isSuccess {
            let json = JSON(response.result.value!)
        } else {
            // Handle error
        }
     }

If you are interested in using this pattern in your networking layer but you don't want the baggage of setting it up, the Teeps team has released a framework you can use called BuckoNetworking that is super simple to use.

You can check out the full source code for this post on GitHub.

We have found this pattern to be very powerful so far in our projects. What patterns have you tried?

Let's build your next big thing.

Contact Us