Created
July 19, 2017 08:17
-
-
Save zarghol/d4310251b7cfbeb8ce9e4e3bfc16e2eb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// BaseWebService.swift | |
// | |
import Foundation | |
import UIKit | |
public enum NetworkError: Error { | |
case noData | |
case noGoodResponse | |
case internalError(Error) | |
case badHTTPCode(Int, Data?) | |
case badConnection(String) | |
case criticError(String, String) | |
case networkUnavailable | |
case unwrapError(DataRepresentable, Error) | |
var description: String { | |
switch self { | |
case .noData: | |
return "No data" | |
case .noGoodResponse: | |
return "No response" | |
case .internalError(let error): | |
return "Internal error : \(error.localizedDescription)" | |
case .badHTTPCode(let httpCode, let data): | |
let dataString: String | |
if let data = data, let dataStr = String(data: data, encoding: .utf8) { | |
dataString = dataStr | |
} else { | |
dataString = "" | |
} | |
return "Bad Http Code : \(httpCode) \(dataString)" | |
case .badConnection(let message): | |
return message | |
case .criticError(let title, let message): | |
return "\(title) : \(message)" | |
case .networkUnavailable: | |
return "network unavailable" | |
case .unwrapError(let data, let error): | |
return "unable to unwrap data : \(data) : \(error)" | |
} | |
} | |
} | |
extension NetworkError: Equatable { | |
public static func == (lhs: NetworkError, rhs: NetworkError) -> Bool { | |
switch (lhs, rhs) { | |
case (.noData, .noData), (.noGoodResponse, .noGoodResponse), (.networkUnavailable, .networkUnavailable): | |
return true | |
case (.internalError(let err1), .internalError(let err2)): | |
return err1.localizedDescription == err2.localizedDescription | |
case (.badHTTPCode(let httpCode1, let data1), .badHTTPCode(let httpCode2, let data2)): | |
return httpCode1 == httpCode2 && data1 == data2 | |
case (.badConnection(let message1), .badConnection(let message2)): | |
return message1 == message2 | |
case (.criticError(let title1, let message1), .criticError(let title2, let message2)): | |
return title1 == title2 && message1 == message2 | |
default: | |
return false | |
} | |
} | |
} | |
fileprivate var currentRequests: Int = 0 { | |
didSet { | |
if currentRequests == 0 && oldValue > 0 { | |
DispatchQueue.main.async { | |
UIApplication.shared.isNetworkActivityIndicatorVisible = false | |
} | |
} else if currentRequests > 0 && oldValue == 0 { | |
DispatchQueue.main.async { | |
UIApplication.shared.isNetworkActivityIndicatorVisible = true | |
} | |
} | |
} | |
} | |
// MARK: - DataRepresentable | |
public protocol DataRepresentable { | |
func toData() throws -> Data | |
} | |
extension Data: DataRepresentable { | |
public func toData() throws -> Data { | |
return self | |
} | |
} | |
enum StringError: Error { | |
case unableToBuildData(String) | |
} | |
extension String: DataRepresentable { | |
public func toData() throws -> Data { | |
if let data = self.data(using: .utf8, allowLossyConversion: false) { | |
return data | |
} else { | |
throw StringError.unableToBuildData(self) | |
} | |
} | |
} | |
// MARK: - WS protocols | |
enum WSResult<T> { | |
case success(T) | |
case error(NetworkError) | |
} | |
protocol Service { | |
var isReachable: Bool { get } | |
mutating func cancelAll() | |
} | |
protocol DataService: Service { | |
func downloadData(at url: URL, completion: ((WSResult<Data>) -> Void)?) | |
func downloadData(with request: URLRequest, completion: ((WSResult<Data>) -> Void)?) | |
} | |
protocol SendService: Service { | |
func send(data: DataRepresentable, at url: URL, completion: ((WSResult<Data>) -> Void)?) | |
func send(data: DataRepresentable, with request: URLRequest, completion: ((WSResult<Data>) -> Void)?) | |
} | |
protocol FileService: Service { | |
func downloadFile(at url: URL, completion: ((WSResult<URL>) -> Void)?) | |
func downloadFile(with request: URLRequest, completion: ((WSResult<URL>) -> Void)?) | |
} | |
// MARK: - concrete implementation | |
protocol WebService: Service { | |
var session: URLSession { get } | |
} | |
extension WebService { | |
/// cancel all current task | |
mutating func cancelAll() { | |
self.session.getTasksWithCompletionHandler { dataTasks, uploadTasks, downloadTasks in | |
dataTasks.forEach { $0.cancel() } | |
uploadTasks.forEach { $0.cancel() } | |
downloadTasks.forEach { $0.cancel() } | |
} | |
} | |
/// Generate completion for all task : check errors, and execute completion if possible | |
func taskCompletion<T>(completion: ((WSResult<T>) -> Void)? = nil) -> ((T?, URLResponse?, Error?) -> Void) { | |
return { dataOrOther, response, error in | |
currentRequests -= 1 | |
do { | |
let data = try self.checkCommonErrorsFor(data: dataOrOther, response: response, error: error) | |
completion?(.success(data)) | |
} catch let error as NetworkError { | |
completion?(.error(error)) | |
} catch { | |
print("error occured : \(error)") | |
completion?(.error(NetworkError.internalError(error))) | |
} | |
} | |
} | |
func checkCommonErrorsFor<T>(data: T?, response: URLResponse?, error: Error?) throws -> T { | |
if let error = error { | |
throw NetworkError.internalError(error) | |
} | |
guard let httpResp = response as? HTTPURLResponse else { | |
throw NetworkError.noGoodResponse | |
} | |
guard httpResp.statusCode == 200 else { | |
throw NetworkError.badHTTPCode(httpResp.statusCode, data as? Data) | |
} | |
guard let data = data else { | |
throw NetworkError.noData | |
} | |
return data | |
} | |
} | |
protocol WebDataService: WebService, DataService { } | |
extension WebDataService { | |
/** | |
Download datas with get request from url. | |
:param: url the url to go to get datas. | |
:param: completion stuff to do with received datas (Asynchrone call) | |
*/ | |
func downloadData(at url: URL, completion: ((WSResult<Data>) -> Void)? = nil) { | |
self.sendRequest(url: url, completion: completion) | |
} | |
func downloadData(with request: URLRequest, completion: ((WSResult<Data>) -> Void)? = nil) { | |
self.sendRequest(request: request, completion: completion) | |
} | |
func sendRequest(url: URL, completion: ((WSResult<Data>) -> Void)?) { | |
guard isReachable else { | |
completion?(.error(.networkUnavailable)) | |
return | |
} | |
let task = self.session.dataTask(with: url, | |
completionHandler: self.taskCompletion(completion: completion)) | |
currentRequests += 1 | |
task.resume() | |
} | |
func sendRequest(request: URLRequest, completion: ((WSResult<Data>) -> Void)? = nil) { | |
guard isReachable else { | |
completion?(.error(.networkUnavailable)) | |
return | |
} | |
let task = self.session.dataTask(with: request, | |
completionHandler: self.taskCompletion(completion: completion)) | |
currentRequests += 1 | |
task.resume() | |
} | |
} | |
protocol WebSendService: WebService, SendService { } | |
extension WebSendService { | |
/** | |
Send POST data to an url. | |
:param: data The data object to send. | |
:param: url Where to send data. | |
:param: completion stuff to do with received datas (Asynchrone call) | |
*/ | |
func send(data: DataRepresentable, at url: URL, completion: ((WSResult<Data>) -> Void)?) { | |
var request = URLRequest(url: url) | |
request.httpMethod = "POST" | |
self.send(data: data, with: request, completion: completion) | |
} | |
/** | |
Send a custom request to an url. | |
:param: data The data object to send. | |
:param: url Where to send data. | |
:param: completion stuff to do with received datas (Asynchrone call) | |
*/ | |
func send(data: DataRepresentable, with request: URLRequest, completion: ((WSResult<Data>) -> Void)?) { | |
guard isReachable else { | |
completion?(.error(.networkUnavailable)) | |
return | |
} | |
do { | |
let data = try data.toData() | |
let task = self.session.uploadTask(with: request, | |
from: data, | |
completionHandler: self.taskCompletion(completion: completion)) | |
currentRequests += 1 | |
task.resume() | |
} catch { | |
completion?(.error(.internalError(error))) | |
} | |
} | |
} | |
protocol WebFileService: WebService, FileService { } | |
extension WebFileService { | |
/** | |
Download file with get request from url. | |
:param: url the url to go to get datas. | |
:param: completion stuff to do with received datas (Asynchrone call) | |
*/ | |
func downloadFile(at url: URL, completion: ((WSResult<URL>) -> Void)?) { | |
guard isReachable else { | |
completion?(.error(.networkUnavailable)) | |
return | |
} | |
let task = self.session.downloadTask(with: url, | |
completionHandler: self.taskCompletion(completion: completion)) | |
currentRequests += 1 | |
task.resume() | |
} | |
func downloadFile(with request: URLRequest, completion: ((WSResult<URL>) -> Void)?) { | |
guard isReachable else { | |
completion?(.error(.networkUnavailable)) | |
return | |
} | |
let task = self.session.downloadTask(with: request, completionHandler: self.taskCompletion(completion: completion)) | |
currentRequests += 1 | |
task.resume() | |
} | |
} | |
class BaseWebService: NSObject, WebService { | |
internal lazy var session: URLSession = { | |
let conf = URLSessionConfiguration.default | |
if #available(iOS 11.0, *) { | |
conf.waitsForConnectivity = true | |
// conf.timeoutIntervalForRequest | |
conf.timeoutIntervalForResource = 30.0 | |
} | |
return URLSession(configuration: conf)//, delegate: self, delegateQueue: nil) | |
}() | |
// private let networkQueue = DispatchQueue(label: "networking", qos: DispatchQoS.background) | |
let reachability: Reachability? = { | |
if #available(iOS 11.0, *) { | |
return nil | |
} else { | |
let reach = Reachability.forInternetConnection() | |
reach?.startNotifier() | |
return reach | |
} | |
}() | |
var isReachable: Bool { | |
return self.reachability?.isReachable() ?? true | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment