David Yang

Tips and posts for iOS developers from an iOS developer.

A network layer is required for any apps that consumes an API. In this article, I will present my own implementation of a network layer in Swift (heavily inspired by Moya), something I actually use in small projects.

Why write your own?

There are lots of tools already available from the community, the most famous ones are probably Alamofire, AFNetworking (mostly known by Objective-C developers), Moya…

The reason why I would use my own simply depends on the complexity of the project I work on. If it has a simple goal and does not rely heavily on APIs, I like to rely on my own code and not depend on a third-party library.

Plus, being my own, I heavily re-use it and it serves as a code snippet for any small project.

How will it work?

Requirements

My small network layer library will consist in the following elements:

  • a Service protocol: exposing properties that will define my API services. By writing an enum implementing this protocol, you will be able to define your API entrypoints.
  • a ServiceProvider<T: Service> class: the object that will actually handle the API calls with a URLSession and return the responses.

Here is what they look like.

In a Service.swift file:

import Foundation

enum ServiceMethod: String {
    case get = "GET"
    // implement more when needed: post, put, delete, patch, etc.
}

protocol Service {
    var baseURL: String { get }
    var path: String { get }
    var parameters: [String: Any]? { get }
    var method: ServiceMethod { get }
}

extension Service {
    public var urlRequest: URLRequest {
        guard let url = self.url else {
            fatalError("URL could not be built")
        }
        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue

        return request
    }

    private var url: URL? {
        var urlComponents = URLComponents(string: baseURL)
        urlComponents?.path = path

        if method == .get {
            // add query items to url
            guard let parameters = parameters as? [String: String] else {
                fatalError("parameters for GET http method must conform to [String: String]")
            }
            urlComponents?.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
        }

        return urlComponents?.url
    }
}

And my ServiceProvider.swift file:

import Foundation

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

class ServiceProvider<T: Service> {
    var urlSession = URLSession.shared

    init() { }

    func load(service: T, completion: @escaping (Result<Data>) -> Void) {
        call(service.urlRequest, completion: completion)
    }

    func load<U>(service: T, decodeType: U.Type, completion: @escaping (Result<U>) -> Void) where U: Decodable {
        call(service.urlRequest) { result in
            switch result {
            case .success(let data):
                let decoder = JSONDecoder()
                do {
                    let resp = try decoder.decode(decodeType, from: data)
                    completion(.success(resp))
                }
                catch {
                    completion(.failure(error))
                }
            case .failure(let error):
                completion(.failure(error))
            case .empty:
                completion(.empty)
            }
        }
    }
}

extension ServiceProvider {
    private func call(_ request: URLRequest, deliverQueue: DispatchQueue = DispatchQueue.main, completion: @escaping (Result<Data>) -> Void) {
        urlSession.dataTask(with: request) { (data, _, error) in
            if let error = error {
                deliverQueue.async {
                    completion(.failure(error))
                }
            } else if let data = data {
                deliverQueue.async {
                    completion(.success(data))
                }
            } else {
                deliverQueue.async {
                    completion(.empty)
                }
            }
            }.resume()
    }
}

How to use it?

Captain Marvel

My example will be based on the Marvel Comics API available here.

import Foundation

enum MarvelService {
    case characters(name: String)
    case character(identifier: String)
}

extension MarvelService: Service {
    var baseURL: String {
        return "https://gateway.marvel.com:443"
    }

    var path: String {
        switch self {
        case .characters(_):
            return "/v1/public/characters"
        case .character(let identifier):
            return "/v1/public/characters/\(identifier)"
        }
    }

    var parameters: [String: Any]? {
        // default params
        var params: [String: Any] = ["apikey": "MY_API_KEY"]
        
        switch self {
        case .characters(let name):
            params["name"] = name
        case .character(_):
            break
        }
        return params
    }

    var method: ServiceMethod {
        return .get
    }
}

In the previous example, I implemented two endpoints:

  • /v1/public/characters by using the .characters enum value: Fetches lists of comic characters with optional filters.
  • /v1/public/characters/{character_id} by using the .character enum value: Fetches a single character resource.

Usage

let provider = ServiceProvider<MarvelService>()

provider.load(service: .characters(name: "spider-man")) { result in
    switch result {
    case .success(let resp):
        print(resp)
    case .failure(let error):
        print(error.localizedDescription)
    case .empty:
        print("No data")
    }
}

Conclusion

With this code snippet, you can set up a pretty simple “vanilla” network layer, relying on URLSession.