Gist for using data delegates in a NetworkSessionInterface
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
// | |
// 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