Gist for using data delegates in a NetworkSessionInterface
// | |
// NetworkSessionInterface.swift | |
// ToyPhotoGallery | |
// | |
// Created by Voxels on 7/6/18. | |
// Copyright © 2018 Michael Edgcumbe. All rights reserved. | |
// | |
import Foundation | |
/// Protocol used to notify delegates of a data task's progress and received bytes | |
protocol NetworkSessionInterfaceDataTaskDelegate : class { | |
var uuid:String { get } | |
func didReceive(data: Data, for uuid:String?) throws | |
func didReceive(response: URLResponse, for uuid:String?) | |
func didFinish(uuid:String?) | |
func didFail(uuid:String?, with error:URLError) | |
} | |
/// Class used to wrap URLSession for handling data and download session tasks | |
class NetworkSessionInterface : NSObject { | |
/// A default configuration used for the URLSession | |
static let defaultConfiguration = URLSessionConfiguration.default | |
/// The default timeout used for URLSession requests | |
static let defaultTimeout:TimeInterval = 30 | |
/// The default cache policy used for URLSession requests | |
static let defaultCachePolicy:URLRequest.CachePolicy = FeaturePolice.disableCache ? .reloadIgnoringLocalAndRemoteCacheData : .returnCacheDataElseLoad | |
/// An operation queue used to facilitate the URLSession | |
let operationQueue = OperationQueue() | |
/// The error handler delegate used to report non-fatal errors | |
let errorHandler:ErrorHandlerDelegate | |
/// A map of the data delegates | |
var dataDelegates = [String:NetworkSessionInterfaceDataTaskDelegate]() | |
/// The URLSession used for requests | |
var session:URLSession? | |
/// The bucket handler for fetching AWS specific URLs | |
lazy var bucketHandler:AWSBucketHandler = AWSBucketHandler() | |
init(with errorHandler:ErrorHandlerDelegate) { | |
self.errorHandler = errorHandler | |
super.init() | |
operationQueue.maxConcurrentOperationCount = 1 | |
session = session(with: FeaturePolice.networkInterfaceUsesEphemeralSession ? .ephemeral : URLSessionConfiguration.default, queue: operationQueue) | |
} | |
/** | |
Constructs a session with the given configuration and queue | |
- parameter configuration: The *URLSessionConfiguration* intended for the session | |
- parameter queue: the *OperationQueue* used to facilitate the session | |
- Returns: a configured *URLSession* | |
*/ | |
func session(with configuration:URLSessionConfiguration, queue:OperationQueue) -> URLSession { | |
if #available(iOS 11.0, *) { | |
configuration.waitsForConnectivity = true | |
} | |
return URLSession(configuration: configuration, delegate: self, delegateQueue: queue) | |
} | |
/** | |
Uses a one-off URLSession, NOT the interface's session, to perform a quick fetch of a data task for the given URL | |
- parameter url: the URL being fetched | |
- parameter queue: The queue that the fetch should be returned on | |
- parameter compeletion: a callback used to pass through the optional fetched *Data* | |
- Returns: void | |
*/ | |
func fetch(url:URL, with session:URLSession? = nil, on queue:DispatchQueue?, cacheHandler:CacheHandler?, delegate:NetworkSessionInterfaceDataTaskDelegate? = nil, completion:@escaping (Data?)->Void) { | |
// Using a default session here may crash because of a potential bug in Foundation. | |
// Ephemeral and Shared sessions don't crash. | |
// See: https://forums.developer.apple.com/thread/66874 | |
var fetchQueue:DispatchQueue = .main | |
if let otherQueue = queue { | |
fetchQueue = otherQueue | |
} | |
if AWSBucketHandler.isAWS(url: url), let filename = bucketHandler.filename(for: url) { | |
bucketHandler.fetchWithAWS(filename: filename, on:fetchQueue, with:errorHandler, completion: completion) | |
return | |
} | |
let useSession = session != nil ? session : FeaturePolice.networkInterfaceUsesEphemeralSession ? URLSession(configuration: .ephemeral) : URLSession(configuration: .default) | |
let taskCompletion:((Data?, URLResponse?, Error?) -> Void) = { [weak self] (data, response, error) in | |
if let e = error { | |
fetchQueue.async { | |
self?.errorHandler.report(e) | |
completion(nil) | |
} | |
return | |
} | |
fetchQueue.async { | |
if let handler = cacheHandler { | |
do { | |
try handler.storeResponse(response: response, data: data, completion: completion) | |
} catch { | |
} | |
} else { | |
completion(data) | |
} | |
} | |
} | |
if useSession?.delegate == nil { | |
guard let task = useSession?.dataTask(with: url, completionHandler: taskCompletion) else { | |
completion(nil) | |
return | |
} | |
task.resume() | |
} else { | |
guard let task = session?.dataTask(with: url) else { | |
completion(nil) | |
return | |
} | |
if let delegate = delegate { | |
let uuidString = UUID().uuidString | |
task.taskDescription = uuidString | |
dataDelegates[uuidString] = delegate | |
} | |
task.resume() | |
} | |
} | |
} | |
extension NetworkSessionInterface : URLSessionTaskDelegate, URLSessionDataDelegate { | |
func dataDelegate(for task:URLSessionTask)->NetworkSessionInterfaceDataTaskDelegate? { | |
guard let taskDescription = task.taskDescription else { | |
return nil | |
} | |
return dataDelegates[taskDescription] | |
} | |
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { | |
if let httpResponse = response as? HTTPURLResponse { | |
if (200...299).contains(httpResponse.statusCode) { | |
// We could also just continue without changing to a download task by using completionHandler(.allow) | |
completionHandler(.becomeDownload) | |
} else { | |
#if DEBUG | |
let logHandler = DebugLogHandler() | |
logHandler.console("Received status code \(httpResponse.statusCode) for url: \(response.url?.absoluteString ?? "unknown")") | |
#endif | |
completionHandler(.cancel) | |
} | |
} else { | |
completionHandler(.cancel) | |
} | |
if let dataDelegate = dataDelegate(for: dataTask) { | |
dataDelegate.didReceive(response: response, for: dataTask.taskDescription) | |
} | |
} | |
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { | |
if let dataDelegate = dataDelegate(for: dataTask) { | |
do { | |
try dataDelegate.didReceive(data: data, for:dataTask.taskDescription) | |
} catch { | |
errorHandler.report(error) | |
} | |
} | |
} | |
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { | |
if let dataTask = task as? URLSessionDataTask, let dataDelegate = dataDelegate(for: dataTask) { | |
if let e = error as? URLError { | |
switch e { | |
case URLError.cancelled: | |
break | |
default: | |
dataDelegate.didFail(uuid:task.taskDescription, with: e) | |
errorHandler.report(e) | |
} | |
} else { | |
dataDelegate.didFinish(uuid:task.taskDescription) | |
} | |
} | |
} | |
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { | |
DispatchQueue.main.async { | |
if let appDelegate = UIApplication.shared.delegate as? AppDelegate, | |
let completionHandler = appDelegate.backgroundSessionCompletionHandler { | |
appDelegate.backgroundSessionCompletionHandler = nil | |
completionHandler() | |
} | |
} | |
} | |
// Unsupported in this version | |
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { | |
let protectionSpace = challenge.protectionSpace | |
guard let serverTrust = protectionSpace.serverTrust else { | |
completionHandler(.performDefaultHandling, nil) | |
return | |
} | |
do { | |
try checkValidity(of:serverTrust) | |
} catch { | |
errorHandler.report(error) | |
completionHandler(.cancelAuthenticationChallenge, nil) | |
} | |
} | |
// TODO: Implement checkValidity | |
func checkValidity(of:SecTrust) throws { | |
throw NetworkError.InvalidServerTrust | |
} | |
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment