Skip to content

Instantly share code, notes, and snippets.

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 voxels/994a8fb6082d190f0b3a6bd08fbcdf8d to your computer and use it in GitHub Desktop.
Save voxels/994a8fb6082d190f0b3a6bd08fbcdf8d to your computer and use it in GitHub Desktop.
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