Skip to content

Instantly share code, notes, and snippets.

@sundeepgupta
Last active June 14, 2017 11:38
Show Gist options
  • Save sundeepgupta/442601b116098059a178eed4dc0255d3 to your computer and use it in GitHub Desktop.
Save sundeepgupta/442601b116098059a178eed4dc0255d3 to your computer and use it in GitHub Desktop.
Code for Roll your own networking in Swift blog article: http://sundeepgupta.ca/post/161796783473/roll-your-own-swift-networking-code
import Foundation
// MARK: - Public
public struct WebService {
let actionManager: ActionManager
// As public API, optimize for simplicity by only providing a Result and not throwing.
func getProfile(id: String, completion: @escaping (Result<Profile>) -> Void) {
let action = Action<NSNull, Profile>(urlString: "api.com/profile", method: "GET", body: NSNull())
perform(action, completion: completion)
}
func createProfile(_ profile: Profile, completion: @escaping (Result<NSNull>) -> Void) {
let action = Action<Profile, NSNull>(urlString: "api.com/profile", method: "POST", body: profile)
perform(action, completion: completion)
}
private func perform<Req, Res>(_ action: Action<Req, Res>, completion: @escaping (Result<Res>) -> Void) {
do { try actionManager.perform(action) { completion($0) } }
catch { completion(Result.failure(error)) }
}
}
public protocol Encodable {
func encode() throws -> Data
}
public protocol Decodable {
static func decode(data: Data) throws -> Self
}
public struct Profile: Encodable, Decodable {
let name: String
let age: Int
public static func decode(data: Data) throws -> Profile {
guard
let object = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let name = object["name"] as? String,
let age = object["age"] as? Int
else { throw WebServiceError.invalidJSON(data) }
return self.init(name: name, age: age)
}
public func encode() throws -> Data {
let object: [String: Any] = ["name": name, "age": age]
return try JSONSerialization.data(withJSONObject: object)
}
}
extension NSNull: Encodable, Decodable {
public static func decode(data: Data) throws -> Self {
return self.init()
}
public func encode() throws -> Data {
return Data()
}
}
public enum Result<Value> {
case success(Value)
case failure(Error)
}
public enum WebServiceError: Error {
case invalidURL(String)
case invalidStatusCode(Int)
case invalidJSON(Data)
}
// MARK: - Internal
internal struct Action<RequestBody: Encodable, ResponseBody: Decodable> {
let urlString: String
let method: String
let body: RequestBody
let decode = ResponseBody.decode
// Anything else you may need like tokens, headers, etc.
func request() throws -> URLRequest {
guard let url = URL(string: urlString) else { throw WebServiceError.invalidURL(urlString) }
var request = URLRequest(url: url)
request.httpMethod = method
request.httpBody = try body.encode()
return request
}
}
internal struct ActionManager {
let httpManager: HTTPManager
func perform<Req, Res>(_ action: Action<Req, Res>, completion: @escaping (Result<Res>) -> Void) throws {
let request = try action.request()
httpManager.perform(request) { result in
switch result {
case .success(let data):
do { completion(Result.success(try action.decode(data))) }
catch { completion(Result.failure(error)) }
case .failure(let error): completion(Result.failure(error))
}
}
}
}
internal struct HTTPManager {
let urlSession: URLSession
func perform(_ request: URLRequest, completion: @escaping (Result<Data>) -> Void) {
urlSession.dataTask(with: request) { data, response, error in
if error != nil { return completion(Result.failure(error!)) }
let response = response as! HTTPURLResponse // Trusting the docs for this example!
// Could pass in an object that deals with status codes, which would be part of the Action.
guard response.statusCode == 200 else {
return completion(Result.failure(WebServiceError.invalidStatusCode(response.statusCode)))
}
completion(Result.success(data ?? Data()))
}.resume()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment