David Yang
Tips and posts for iOS developers from an iOS developer.
Writing your own Network layer in Swift
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?
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.