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?