Simple network synchronizer with caching
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
import Foundation | |
/** | |
The only benefit SessionDelegate provides is to accompany URL respones with cache control headers which are not provided by the backend at the time of writing. | |
(Cache control headers in responses let us use Foundation's NSURLCache abilities.) | |
Once the backend implementation is changed to provide responses with cache control headers SessionDelegate should no longer be used. | |
NOT THREAD SAFE - it should be used on the delegation queue of NSURLSession | |
*/ | |
class SessionDelegate: NSObject { | |
let cacheTime: TimeInterval | |
init(cacheTime: TimeInterval) { | |
self.cacheTime = cacheTime | |
} | |
typealias TaskCompletionHandler = (Data?, URLResponse?, NSError?) -> Void | |
func setCompletionHandlerForTask(_ task: URLSessionDataTask, handler: @escaping TaskCompletionHandler) { | |
completionHandlerForTask[task] = handler | |
} | |
fileprivate var completionHandlerForTask = [URLSessionTask: TaskCompletionHandler]() | |
fileprivate var dataForTask = [URLSessionTask: NSMutableData?]() | |
} | |
extension SessionDelegate: URLSessionTaskDelegate { | |
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { | |
let data = dataForTask[task] ?? nil | |
completionHandlerForTask[task]?(data as Data?, task.response, error as NSError?) | |
completionHandlerForTask[task] = nil | |
} | |
} | |
extension SessionDelegate: URLSessionDataDelegate { | |
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { | |
guard let mutableData = dataForTask[dataTask] else { | |
dataForTask[dataTask] = NSMutableData(data: data) | |
return | |
} | |
mutableData?.append(data) | |
} | |
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> Void) { | |
switch proposedResponse.response { | |
case let response as HTTPURLResponse: | |
var headers = response.allHeaderFields as! [String: String] | |
headers["Cache-Control"] = "max-age=\(cacheTime)" | |
let modifiedResponse = HTTPURLResponse(url: response.url!, | |
statusCode: response.statusCode, | |
httpVersion: "HTTP/1.1", | |
headerFields: headers) | |
let modifiedCachedResponse = CachedURLResponse(response: modifiedResponse!, | |
data: proposedResponse.data, | |
userInfo: proposedResponse.userInfo, | |
storagePolicy: proposedResponse.storagePolicy) | |
completionHandler(modifiedCachedResponse) | |
default: | |
completionHandler(proposedResponse) | |
} | |
} | |
} |
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
import Foundation | |
protocol Resource { | |
func request() -> URLRequest | |
associatedtype ParsedObject | |
var parse: (Data) throws -> ParsedObject { get } | |
} | |
enum SynchronizerResult<Result> { | |
case success(Result) | |
case noData | |
case error(Error) /// Might be SynchronizerError or parsing error thrown by Resource parse function | |
} | |
enum SynchronizerError: Error { | |
case wrongStatusError(status: Int) | |
case urlSessionError(NSError) | |
} | |
class Synchronizer { | |
private lazy var session = self.createSession() | |
private func createSession() -> URLSession { | |
return URLSession( | |
configuration: self.sessionConfiguration, | |
delegate: SessionDelegate(cacheTime: self.cacheTime), | |
delegateQueue: OperationQueue.main | |
) | |
} | |
private var sessionDelegate: SessionDelegate { return session.delegate as! SessionDelegate } | |
private let sessionConfiguration: URLSessionConfiguration | |
private let cacheTime: TimeInterval | |
init(cacheTime: TimeInterval, URLCache: Foundation.URLCache? = URLSessionConfiguration.default.urlCache) { | |
self.cacheTime = cacheTime | |
self.sessionConfiguration = URLSessionConfiguration.default | |
self.sessionConfiguration.urlCache = URLCache | |
} | |
func cancelSession() { | |
session.invalidateAndCancel() | |
session = createSession() | |
} | |
typealias CancelLoading = () -> Void | |
func loadResource<R: Resource, Object> | |
(_ resource: R, completion: @escaping (SynchronizerResult<Object>) -> ()) -> CancelLoading where R.ParsedObject == Object { | |
func completeOnMainThread(_ result: SynchronizerResult<Object>) { | |
if case .error = result { print(result) } | |
OperationQueue.main.addOperation{ completion(result) } | |
} | |
let request = resource.request() | |
let task = session.dataTask(with: request) | |
print("Request: \(request)") | |
sessionDelegate.setCompletionHandlerForTask(task) { (data, response, error) in | |
guard error?.code != NSURLErrorCancelled else { | |
print("Request with URL: \(String(describing: request.url)) was cancelled") | |
return // cancel quitely | |
} | |
print("Response: \(String(describing: response))") | |
if let result = SynchronizerResult<Object>.resultWithResponse(response, error: error) { | |
completeOnMainThread(result) | |
return | |
} | |
guard let data = data, data.count > 0 else { | |
completeOnMainThread(.noData) | |
return | |
} | |
do { | |
let object = try resource.parse(data) | |
completeOnMainThread(.success(object)) | |
} catch { | |
completeOnMainThread(.error(error)) | |
} | |
} | |
task.resume() | |
return { [weak task] in | |
task?.cancel() | |
} | |
} | |
} | |
private extension SynchronizerResult { | |
static func resultWithResponse(_ response: URLResponse?, error: NSError?) -> SynchronizerResult? { | |
guard error == nil else { | |
return self.error(SynchronizerError.urlSessionError(error!)) | |
} | |
let statusCode = (response as! HTTPURLResponse).statusCode | |
guard 200..<300 ~= statusCode else { | |
return self.error(SynchronizerError.wrongStatusError(status: statusCode)) | |
} | |
return nil | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment