Writing an Elegant and Extensible Network Stack in Swift

Peter Livesey
Device Blogs
Published in
6 min readAug 10, 2019

Apple constantly updates their networking APIs to make them easier to use. However, I find that a lot of people still are hesitant to write their own networking layer and choose to rely on common open source solutions.

Writing your own networking stack has several benefits. To name just a few:

  • Avoids a 3rd party dependency which will need upgrading
  • More understandable code (since you wrote it!)
  • More extensible
  • Easier to debug, add logs, and add performance monitoring
  • Much easier to add advanced functionality
  • Allows you to parse errors from your backend into a specific model
  • Makes it easy to support custom error handling such as 401 or 500 status codes

Plus, it’s now just really easy! In this article, I’m going to go over the basics. By the end of this post, you’ll have a network stack that can easily send GET and POST requests with model parsing with little boilerplate.

let request = Request(path: "users/1")
Network.shared.send(request) { (result: Result<User, Error>) in
switch result {
case .success(let user):
print(user)
case .failure(let error):
print(error)
}
}
With codable, NSURLSession, and generics, writing your own networking stack is easier than ever

In a follow-up post, I’ll show how you can upgrade this simple network stack with more advanced features such as background tasks, file downloads, cancelable requests, and more.

Setup

First, let’s create a new file, Network.swift and add a singleton accessor, a session variable, and a basic send function:

class Network {
static let shared = Network()
let session: URLSession = URLSession(configuration: .default)
func send(_ request: URLRequest,
completion: @escaping (Result<Data, Error>)->Void) {}
}

The send function takes a request and a completion block which returns a Result with either data or an error. Now, let’s fill this in with a simple implementation:

Log.verbose("Send: \(urlRequest.url?.absoluteString ?? "")")let task = session.dataTask(with: request) { data, response, error in
let result: Result<T, Error>
if let error = error {
// First, check if the network just returned an error
result = .failure(error)
} else if let data = data {
result = .success(data)
} else {
Log.assertFailure("Missing both data and error from NSURLSession.")
result = .failure(NetworkError.noDataOrError)
}
DispatchQueue.main.async {
completion(result)
}
}
// Make sure to call resume on the task or nothing will happen!
task.resume()

Most of the code here is straightforward, but you’ll notice a couple of interesting things about this implementation:

  1. I created a simple NetworkError enum for some basic errors.
  2. I’ve added some Log statements to the code. This is one of the benefits of your own networking stack! You can add your own Log statements where you see fit. Here’s my implementation or previous post on assertions in code.

Models

This send function works fine, but it isn’t much better than just using URLSession directly. In most cases, we don’t want Data returned, but a model. So, let’s add a Model protocol and add support for parsing.

protocol Model: Codable {}

It’ll become clear in a future post why we create this empty protocol instead of using Codable directly. Just trust me for now.

func send<T: Model>(_ request: URLRequest, completion: @escaping (Result<T, Error>)->Void) {...} else if let data = data {
do {
let decoder = JSONDecoder()
result = .success(try decoder.decode(T.self, from: data))
} catch {
result = .failure(error)
}
}

Simple. We just change the send function to be generic and add the JSONDecoder handling. When we call this function, we’ll be returned a model to immediately use in application code.

Better Requests

The URLRequest object is a little verbose to use, so instead, let’s create a wrapper object to make creating requests easier. Instead of using inheritance, I prefer a protocol-oriented architecture:

protocol Requestable {
func urlRequest() -> URLRequest
}
struct Request: Requestable {
let path: String
let method: String
init(path: String, method: String = "GET") {
self.path = path
self.method = method
}
func urlRequest() -> URLRequest {
guard let url = URL(string: "BASE_URL") else {
Log.assertFailure("Failed to create base url")
return URLRequest(url: URL(fileURLWithPath: ""))
}
var request = URLRequest(url: url.appendingPathComponent(path))
request.httpMethod = method
return request
}
}
extension URLRequest: Requestable {
func urlRequest() -> URLRequest { return self }
}

This new Request object includes the base url for your server so you can simply specify a path and a method. We can also easily make URLRequest adhere to Requestable in case we do want to make requests to other servers.

Updating our send method to use this new protocol is trivial:

func send<T: Model>(_ request: Requestable, completion: @escaping (Result<T, Error>)->Void) {
let urlRequest = request.urlRequest()
...

Post Requests

When sending a POST request, you often send JSON data as part of the request. Of course, we don’t want to format that ourselves. How about using a Model? Let’s add a PostRequest struct:

struct PostRequest<Model: Encodable>: Requestable {
let path: String
let model: Model
func urlRequest() -> URLRequest {
guard let url = URL(string: "BASE_URL") else {
Log.assertFailure("Failed to create base url")
return URLRequest(url: URL(fileURLWithPath: ""))
}
var request = URLRequest(url: url.appendingPathComponent(path))
request.httpMethod = "POST"
do {
let encoder = JSONEncoder()
let data = try encoder.encode(model)
urlRequest.httpBody = data
urlRequest.setValue("application/json",
forHTTPHeaderField: "Content-Type")
} catch let error {
Log.assertFailure("Post request model parsing failed")
}
return urlRequest
}
}

Now, you can send models up to the server as POST requests. For example:

let newUser = User(id: 2, name: "Peter")
let request = PostRequest(path: "/users", model: newUser)
Network.shared.send(request) { (result: Result<Empty, Error>) in
print(result)
}

Status Code Errors

One gotcha of URLSession is that it doesn’t handle status code errors. Luckily, adding logic to parse the status code is easy. And in the future, you could also handle specific status codes like unauthorized errors.

struct StatusCodeError: Error {
let code: Int
}
private func error(from response: URLResponse?) -> Error? {
guard let response = response as? HTTPURLResponse else {
Log.assertFailure("Missing http response")
return nil
}
let statusCode = response.statusCode if statusCode >= 200 && statusCode <= 299 {
return nil
} else {
Log.error("Invalid status code: \(statusCode)")
return StatusCodeError(code: statusCode)
}
}

Next, add this check to the send function:

if let error = error {
result = .failure(error)
} else if let error = self.error(from: response) {
result = .failure(error)
} else if let data = data {
...

Threading

And for the last touch, let’s make this a bit more performant. URLSession already runs all networking operations on a background thread, and our model parsing for Get requests is on a background thread. But, when urlRequest() is called, it may cause some JSON parsing to run on the main thread. I recommend wrapping everything inside of send inside of an async block. With that change, send won’t do any work on the main thread.

Getting the Code

If you’d prefer to just copy-paste code into your own project, you can find all the code for a full network stack (and a few examples) in this Github project. The code for this article is shown in SimpleNetwork.swift.

See It in Action

I wrote a similar network stack to this in the Navigator app, which helps make your meetings more productive. Of course, you can’t actually see the network stack…but hopefully, you can feel the elegant code through the app 😉. And, if you’re interested in working with code like this, they’re hiring.

Next time…

This post just outlines the basics of getting a lightweight network stack up and running. With some customization, this should be enough for most apps. In the next post, I’ve included some more advanced features that really show the power and benefit of building your own stack.

This project and article will need to evolve as Swift and Foundation change. If you have any suggestions, comments, improvements, bugs, please let me know in the comments or submit a PR to the Github project.

Thanks to Alice Avery and Kamilah Taylor for reviewing this post.

--

--