Skip to content

Instantly share code, notes, and snippets.

@danielgarbien
Last active March 14, 2018 06:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danielgarbien/8e904b07c07110a502b3116576afaa64 to your computer and use it in GitHub Desktop.
Save danielgarbien/8e904b07c07110a502b3116576afaa64 to your computer and use it in GitHub Desktop.
Simple network synchronizer with caching
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)
}
}
}
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