Scroll back to the top

Simple networking in Swift

Networking code, especially in iOS apps, has a tendency to get ugly. It’s easy to start shoving repeated boilerplate into View Controllers which pushes them even further away from their purpose: managing views. In this article, I will implement a lightweight and flexible way to manage this ugly code which will make our code much more testable, robust, and easy to read.

Note: this method was inspired a wonderful episode of Swift Talk, check it out if you want to learn more!

The problem

Say we have a UIViewController subclass named FriendsViewController which displays a list of the user’s friends. Inside it we have a loadFriends() method.

func loadFriends() {
    let url = URL(string: "https://friends.com/api/friends")!
    URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
        if let data = data, let newFriends = try? JSONDecoder().decode([Friend].self, from: data) {
            self?.friends = newFriends
            self?.tableView.reloadData()
        } else {
            print("FriendsViewController.loadFriends(): Failed to get friends")
        }
    }.resume()
}

This simple method makes an HTTP request via the shared URLSession and attempts to decode an array of Friend s from the data received from the HTTP request.

This code absoutely works, although as the app grows in complexity, it would definitely cause some growing pains. Here’s some things that we could fix:

  • All errors get thrown away

If something goes wrong with the request or if the returned response can’t be parsed, this code is unable to tell the difference. This can make debugging a huge pain.

  • Lots of repeated code

As you add more and more network requests using methods like these, you’ll notice how much code is being repeated. For each method, you need to write:

let url = URL(string: "https://friends.com/api/friends")!
URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
    if let data = data, let newFriends = try? JSONDecoder().decode([Friend].self, from: data) {...

This can get very burdensome and it shifts the focus away from what the method is actually trying to do: populate the friends array.

What would be really nice is if we could have a way to easily specify new network requests, and pass them into a generic function that will return whatever data we’re trying to get.

The Resource type

In theory, each network request has two unique properties: 1) the URL to get data from 2) how to parse the data

Let’s express this idea as a type.

struct Resource<T> {
    let url: URL
    let parse: (Data) -> Result<T>
}

The parse function takes in a Data object and returns an object of type Result<T>. Result types are a great way to handle failures since they avoid the ugly verbosity of exceptions and provide more information than just using Optionals. In Swift, they’re super easy to implement as a generic enum.

enum Result<T> {
    case success(T)
    case failure(NetworkError)
}

ApiService

To have a place to use any Resource s we create, let’s define a protocol that specifies the behavior of fetching data from a URL.

protocol ApiService {
    func load<T>(_ resource: Resource<T>, then completion: @escaping (Result<T>) -> ())
}

And now, let’s make a simple implementation:

final class WebApiService: ApiService {
    let urlSession: URLSession
    
    func load<T>(_ resource: Resource<T>, then completion: @escaping (Result<T>) -> ()) {
        urlSession.dataTask(with: resource.url) { (data, _, _) in
            guard let data = data else {
                completion(.failure(NetworkError(message: "Failed to load data.")))
                return
            }
            completion(resource.parse(data))
        }.resume()
    }
    
    init(urlSession: URLSession = URLSession.shared) {
        self.urlSession = urlSession
    }
}

This class handles all of the ugly details of creating and running URL sessions. It creates a requst based on the resource’s URL and parses the Data object using the resource’s parse function.

The class also uses dependency injection to allow the user to specify their own URLSession instead of assuming you want to use the default .shared. This makes testing much easier, but that is outside the scope of this article.

Parsing JSON

Let’s first extract some behavior that is common to every network request: parsing JSON.

func parseJson<T: Decodable>(from data: Data) -> Result<T> {
    do {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase
        
        let decodedItem = try decoder.decode(T.self, from: data)
        return .success(decodedItem)
    } catch {
        return .failure(NetworkError(message: error.localizedDescription))
    }
}

This function takes the same signature that is required by the Resource struct.

Creating Resources

I like to create Resource s using extensions on my model types. So, to define a network request that would fetch all Friends for a User, you could write it like this:

extension User {
    var allFriends: Resource<[Friend]> {
        return Resource(url: URL(string: "https://friends.com/api/user/\(id)/friends")!, parse: parseJson)
    }
}

At the call side, you now end up with a beautiful API:

WebApiService().load(user.allFriends) { [weak self] result in
    switch result {
        case .success(let friends):
            self?.friends = friends
            self?.tableView.reloadData()
        case .failure(let error):
	    print(error.message)
    }
}

It’s crystal clear what the point of this code is: populate the friends array with the result of a network request. Compare this code with the original code which was cluttered with implementation details of URLSession and decoding JSON and it will be hard to go back to writing things the old way again!

Conclusion

By extracting away ugly networking code and utilizing Swift’s advanced language features, you can create beautiful, extensible APIs and chip away at classic problems like Massive View Controllers. Our simple abstractions actually make work easier for the programmer, which after all is what the goal for writing abstractions should be.

I hope you enjoyed this article! I use a similar implementation in my open source live music streaming app, Attics. It’s worked great for me so far, and I hope it will work well in your app too. Please feel free to reach out to me in the comments or on Twitter if you have any questions!