Skip to content

Instantly share code, notes, and snippets.

@slycrel
Created August 30, 2017 06:48
Show Gist options
  • Save slycrel/a9b48255e36e6dae3cba816e933c7e1a to your computer and use it in GitHub Desktop.
Save slycrel/a9b48255e36e6dae3cba816e933c7e1a to your computer and use it in GitHub Desktop.
Simple Swift REST Networking Layer
// Copyright © 2016 Jeremy Stone, released under the MIT license.
import UIKit
import Foundation
// Some keychain code relies on Sam Soffes' fantastic keychain library. https://github.com/soffes/SAMKeychain
// For this gist, that code is stubbed to do nothing.
class SAMKeychain {
static func password(forService:String, account:String) -> String? { return nil }
static func deletePassword(forService:String, account:String) { }
static func setPassword(_ password:String, forService:String, account:String) { }
}
enum HTTP_STATUS_CODES:Int {
// 1xx - Informational
case CONTINUE = 100
case SWITCHING_PROTOCOLS = 101
// 2xx - Successful
case INVALID_STATUS = 0
case OK = 200
case CREATED = 201
case ACCEPTED = 202
case NON_AUTHORITATIVE_INFO = 203
case NO_CONTENT = 204
case RESET_CONTENT = 205
case PARTIAL_CONTENT = 206
// 3xx - Redirection
case MULTIPLE_CHOICES = 300
case MOVED_PERMANENTLY = 301
case FOUND = 302
case SEE_OTHER = 303
case NOT_MODIFIED = 304
case USE_PROXY = 305
case TEMPORARY_REDIRECT = 307
// 4xx - Client Error
case BAD_REQUEST = 400
case UNAUTHORIZED = 401
case FORBIDDEN = 403
case NOT_FOUND = 404
case METHOD_NOT_ALLOWED = 405
case NOT_ACCEPTABLE = 406
case PROXY_AUTH_REQUIRED = 407
case CONFLICT = 409
case GONE = 410
case LENGTH_REQUIRED = 411
case PRECONDITION_FAILED = 412
case REQUEST_ENTITY_TOO_LARGE = 413
case REQUEST_URI_TOO_LONG = 414
case UNSUPPORTED_MEDIA_TYPE = 415
case RANGE_NOT_SATISFIABLE = 416
case EXPECTATION_FAILED = 417
// 5xx - Server error
case SERVER_ERROR = 500
case NOT_IMPLEMENTED = 501
case BAD_GATEWAY = 502
case SERVICE_UNAVAILABLE = 503
case GATEWAY_TIMEOUT = 504
case HTTP_VERSION_UNSUPPORTED = 505
}
typealias NetworkErrorHandler = (_ response:HTTPURLResponse?, _ error:NSError?) -> String
typealias NetworkRequestCompletion = (_ data:[String: Any]?, _ response:HTTPURLResponse?, _ error:NSError?) -> Void
let NETWORK_REQUEST_LOGGING = true // set this to true for debug logging
let API_HOST_DEBUG = "" // define to have a "base" url that gets used for partial paths being passed to networking calls.
let API_HOST_PRODUCTION = ""
class NetworkHelper: NSObject {
// internals
private static var _sharedInstance: NetworkHelper?
static var sharedInstance:NetworkHelper {
get {
if _sharedInstance == nil {
_sharedInstance = NetworkHelper()
}
return _sharedInstance!
}
set(instance) {
_sharedInstance = instance
}
}
lazy var session: URLSession = URLSession(configuration: NetworkHelper.sharedInstance.defaultConfiguration());
var host: String
let SAVED_USERNAME = "login username"
let PASSWORD_STORAGE_NAME = "custom app name here"
let HOST_STORAGE_NAME = "NetworkHelperHostStorage"
// MARK: -
override init() {
#if DEBUG // true if "DEBUG=1" is defined in your 'preprocessor macros' build settings for debug builds
host = (UserDefaults.standard.object(forKey: HOST_STORAGE_NAME) as? String) ?? API_HOST_DEBUG
#else
host = (UserDefaults.standard.object(forKey: HOST_STORAGE_NAME) as? String) ?? API_HOST_PRODUCTION
#endif
}
func defaultConfiguration() -> URLSessionConfiguration {
let username = savedUsername()
let password = savedPassword()
let config = URLSessionConfiguration.ephemeral
let base64EncodedCredential = Data("\(username):\(password)".utf8).base64EncodedString()
let authString = "Basic \(base64EncodedCredential)"
config.httpAdditionalHeaders = [
"Accept": "application/json", // modify or add headers here as needed.
"Authorization": authString // Basic Authentication. You can change or remove this for another type of authentication or no authentication.
]
return config
}
private func savedUsername() -> String {
return UserDefaults.standard.string(forKey: SAVED_USERNAME) ?? "username"
}
private func savedPassword() -> String {
return SAMKeychain.password(forService: PASSWORD_STORAGE_NAME, account: savedUsername()) ?? "password"
}
private func savedPasscode() -> String {
return SAMKeychain.password(forService: PASSWORD_STORAGE_NAME, account: "\(savedUsername())-Passcode") ?? ""
}
private func removePasscode() {
SAMKeychain.deletePassword(forService: PASSWORD_STORAGE_NAME, account: "\(savedUsername())-Passcode")
}
private func removePassword() {
SAMKeychain.deletePassword(forService: PASSWORD_STORAGE_NAME, account: savedUsername())
}
private func removeUsername() {
UserDefaults.standard.removeObject(forKey: SAVED_USERNAME)
removePassword()
}
private func saveUsername(username: String, password: String) {
UserDefaults.standard.set(username, forKey: SAVED_USERNAME)
SAMKeychain.setPassword(password, forService: PASSWORD_STORAGE_NAME, account: username)
}
private func savePasscode(passcode:String) {
SAMKeychain.setPassword(passcode, forService: PASSWORD_STORAGE_NAME, account: "\(savedUsername())-Passcode")
}
// MARK: - internals
private func makeRequest(inPath:String, params:[String:String]?, postData:Data?, callType:NetworkHelper.NetworkCallType, completion:NetworkRequestCompletion?) -> URLSessionDataTask {
let finalPath = inPath.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed) ?? inPath
var url = URL(string: finalPath)!
if !(url.absoluteString.contains("https://")) {
let urlString = inPath.hasPrefix("/") ? String(inPath.characters.dropFirst()) : inPath
url = URL(string: "https://\(NetworkHelper.sharedInstance.host)")!
url.appendPathComponent(urlString)
}
if let paramsPassed = params {
var paramsArray = Array<URLQueryItem>()
for (key, value) in paramsPassed {
let item = URLQueryItem(name: key, value: value)
paramsArray.append(item)
}
if var components = URLComponents(string: url.absoluteString) {
components.queryItems = paramsArray
url = components.url!
}
}
var request = URLRequest(url: url)
switch callType {
case .GET: break // default
case .PUT:
request.httpMethod = "PUT"
case .POST:
request.httpMethod = "POST"
case .DELETE:
request.httpMethod = "DELETE"
}
let task = session.dataTask(with: request) { (inData, inResponse, inError) in
var dict:[String:Any]?
if let data = inData {
do {
try dict = JSONSerialization.jsonObject(with: data, options:[]) as? [String: Any]
if NETWORK_REQUEST_LOGGING {
let log = "\n\n*** Request: \(request.url?.absoluteString ?? "?")" +
"\n\n*** Response:\n\(String(data: data, encoding: .utf8) ?? "?")" +
"\n\n*** Response Status: \((inResponse as? HTTPURLResponse)?.statusCode ?? 0)\n\n"
print(log)
}
} catch {
if NETWORK_REQUEST_LOGGING {
let str = String(data: data, encoding: .utf8)
print("\n\n*** Response URL: \(inResponse?.url?.absoluteString ?? "(Response URL Unavailable)")")
print("*** Response Error: \(inError?.localizedDescription ?? "(Blank error)")")
print("*** Serialization failed for data:\n\(str ?? "nil")")
}
dict = ["SRSerializationError": error,
"data": data,
"error": inError ?? NSNull(),
"response": inResponse ?? NSNull()]
}
if let response = inResponse as? HTTPURLResponse {
var error = inError
let statusCode = response.statusCode
if statusCode < HTTP_STATUS_CODES.OK.rawValue || statusCode >= HTTP_STATUS_CODES.MULTIPLE_CHOICES.rawValue {
error = NSError(domain: "RESTErrorDomain", code: statusCode, userInfo: ["data":data])
}
if let compl = completion {
DispatchQueue.main.async {
compl(dict, response, error as NSError?);
}
}
}
}
}
task.resume() // run the task we just built.
return task;
}
// MARK: - Public Functions
enum NetworkCallType {
case GET, PUT, POST, DELETE
}
@discardableResult
static func GET(_ path:String, completion:NetworkRequestCompletion?) -> URLSessionDataTask {
return NetworkHelper.sharedInstance.makeRequest(inPath: path, params: nil, postData: nil, callType: .GET, completion: completion)
}
@discardableResult
static func PUT(_ path:String, params:[String:String]?, completion:NetworkRequestCompletion?) -> URLSessionDataTask {
return NetworkHelper.sharedInstance.makeRequest(inPath: path, params: params, postData: nil, callType: .PUT, completion: completion)
}
@discardableResult
static func POST(_ path:String, params:[String:String]?, postData:Data?, completion:NetworkRequestCompletion?) -> URLSessionDataTask {
return NetworkHelper.sharedInstance.makeRequest(inPath: path, params: params, postData: postData, callType: .POST, completion: completion)
}
@discardableResult
static func DELETE(_ path:String, completion:NetworkRequestCompletion?) -> URLSessionDataTask {
return NetworkHelper.sharedInstance.makeRequest(inPath: path, params: nil, postData: nil, callType: .DELETE, completion: completion)
}
static func clearSession() {
// Note: clearing just the session vs re-making the singleton is effectively the same result. This reduces logical paths of cunstruction vs partial construction.
NetworkHelper._sharedInstance = nil
}
static func setHost(_ host:String) {
if host != NetworkHelper.sharedInstance.host {
NetworkHelper.sharedInstance.host = host
UserDefaults.standard.set(host, forKey: NetworkHelper.sharedInstance.HOST_STORAGE_NAME)
self.clearSession()
}
}
static func isDebugServer() -> Bool {
return NetworkHelper.sharedInstance.host != API_HOST_PRODUCTION
}
static func savedUsername() -> String {
return NetworkHelper.sharedInstance.savedUsername()
}
static func savedPassword() -> String {
return NetworkHelper.sharedInstance.savedPassword()
}
static func savedPasscode() -> String {
return NetworkHelper.sharedInstance.savedPasscode()
}
static func saveUsername(_ username: String, password: String) {
NetworkHelper.sharedInstance.saveUsername(username: username, password: password)
}
static func savePasscode(_ passcode: String) {
NetworkHelper.sharedInstance.savePasscode(passcode: passcode)
}
static func removePasscode() {
NetworkHelper.sharedInstance.removePasscode()
}
static func removeUsername() {
NetworkHelper.sharedInstance.removeUsername()
}
static func readableErrorString(response: HTTPURLResponse?, error:NSError?, handler:NetworkErrorHandler?) -> String {
var str = "Unexpected Error Processing Request"
if let resp = response, let err = error {
if resp.statusCode == 0 && err.code == NSURLErrorNotConnectedToInternet {
return err.localizedDescription
}
}
if let handl = handler {
str = handl(response, error)
} else {
switch response?.statusCode ?? 0 {
case HTTP_STATUS_CODES.SERVER_ERROR.rawValue:
str = "Error Processing Request"
default: break
}
}
if NetworkHelper.isDebugServer() {
DispatchQueue.main.async {
let notificationText = "Dev Error Notification: \(error?.code ?? 0), \(error?.localizedDescription ?? "Unknown Error")"
print(notificationText) // you can show a developer only notification to the end-user here if desired
}
}
return str
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment