You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

249 lines
9.0 KiB

// The MIT License (MIT)
//
// Copyright (c) 2015-2021 Alexander Grebenyuk (github.com/kean).
import Foundation
public protocol Cancellable: AnyObject {
func cancel()
}
public protocol DataLoading {
/// - parameter didReceiveData: Can be called multiple times if streaming
/// is supported.
/// - parameter completion: Must be called once after all (or none in case
/// of an error) `didReceiveData` closures have been called.
func loadData(with request: URLRequest,
didReceiveData: @escaping (Data, URLResponse) -> Void,
completion: @escaping (Error?) -> Void) -> Cancellable
/// Removes data for the given request.
func removeData(for request: URLRequest)
}
extension URLSessionTask: Cancellable {}
/// Provides basic networking using `URLSession`.
public final class DataLoader: DataLoading, _DataLoaderObserving {
public let session: URLSession
private let impl = _DataLoader()
public var observer: DataLoaderObserving?
weak var pipeline: ImagePipeline?
deinit {
session.invalidateAndCancel()
#if TRACK_ALLOCATIONS
Allocations.decrement("DataLoader")
#endif
}
/// Initializes `DataLoader` with the given configuration.
/// - parameter configuration: `URLSessionConfiguration.default` with
/// `URLCache` with 0 MB memory capacity and 150 MB disk capacity.
public init(configuration: URLSessionConfiguration = DataLoader.defaultConfiguration,
validate: @escaping (URLResponse) -> Swift.Error? = DataLoader.validate) {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
self.session = URLSession(configuration: configuration, delegate: impl, delegateQueue: queue)
self.impl.validate = validate
self.impl.observer = self
#if TRACK_ALLOCATIONS
Allocations.increment("DataLoader")
#endif
}
// Performance optimization to reduce number of context switches.
func attach(pipeline: ImagePipeline) {
self.pipeline = pipeline
self.session.delegateQueue.underlyingQueue = pipeline.queue
}
/// Returns a default configuration which has a `sharedUrlCache` set
/// as a `urlCache`.
public static var defaultConfiguration: URLSessionConfiguration {
let conf = URLSessionConfiguration.default
conf.urlCache = DataLoader.sharedUrlCache
return conf
}
/// Validates `HTTP` responses by checking that the status code is 2xx. If
/// it's not returns `DataLoader.Error.statusCodeUnacceptable`.
public static func validate(response: URLResponse) -> Swift.Error? {
guard let response = response as? HTTPURLResponse else {
return nil
}
return (200..<300).contains(response.statusCode) ? nil : Error.statusCodeUnacceptable(response.statusCode)
}
#if !os(macOS) && !targetEnvironment(macCatalyst)
private static let cachePath = "com.github.kean.Nuke.Cache"
#else
private static let cachePath: String = {
let cachePaths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)
if let cachePath = cachePaths.first, let identifier = Bundle.main.bundleIdentifier {
return cachePath.appending("/" + identifier)
}
return ""
}()
#endif
/// Shared url cached used by a default `DataLoader`. The cache is
/// initialized with 0 MB memory capacity and 150 MB disk capacity.
public static let sharedUrlCache: URLCache = {
let diskCapacity = 150 * 1024 * 1024 // 150 MB
#if targetEnvironment(macCatalyst)
return URLCache(memoryCapacity: 0, diskCapacity: diskCapacity, directory: URL(fileURLWithPath: cachePath))
#else
return URLCache(memoryCapacity: 0, diskCapacity: diskCapacity, diskPath: cachePath)
#endif
}()
public func loadData(with request: URLRequest,
didReceiveData: @escaping (Data, URLResponse) -> Void,
completion: @escaping (Swift.Error?) -> Void) -> Cancellable {
return loadData(with: request, isConfined: false, didReceiveData: didReceiveData, completion: completion)
}
func loadData(with request: URLRequest,
isConfined: Bool,
didReceiveData: @escaping (Data, URLResponse) -> Void,
completion: @escaping (Swift.Error?) -> Void) -> Cancellable {
return impl.loadData(with: request, session: session, isConfined: isConfined, didReceiveData: didReceiveData, completion: completion)
}
public func removeData(for request: URLRequest) {
session.configuration.urlCache?.removeCachedResponse(for: request)
}
/// Errors produced by `DataLoader`.
public enum Error: Swift.Error, CustomDebugStringConvertible {
/// Validation failed.
case statusCodeUnacceptable(Int)
public var debugDescription: String {
switch self {
case let .statusCodeUnacceptable(code):
return "Response status code was unacceptable: \(code.description)"
}
}
}
// MARK: _DataLoaderObserving
func dataTask(_ dataTask: URLSessionDataTask, didReceiveEvent event: DataTaskEvent) {
observer?.dataLoader(self, urlSession: session, dataTask: dataTask, didReceiveEvent: event)
}
}
// Actual data loader implementation. Hide NSObject inheritance, hide
// URLSessionDataDelegate conformance, and break retain cycle between URLSession
// and URLSessionDataDelegate.
private final class _DataLoader: NSObject, URLSessionDataDelegate {
var validate: (URLResponse) -> Swift.Error? = DataLoader.validate
private var handlers = [URLSessionTask: _Handler]()
weak var observer: _DataLoaderObserving?
/// Loads data with the given request.
func loadData(with request: URLRequest,
session: URLSession,
isConfined: Bool,
didReceiveData: @escaping (Data, URLResponse) -> Void,
completion: @escaping (Error?) -> Void) -> Cancellable {
let task = session.dataTask(with: request)
let handler = _Handler(didReceiveData: didReceiveData, completion: completion)
if isConfined {
handlers[task] = handler
} else {
session.delegateQueue.addOperation { // `URLSession` is configured to use this same queue
self.handlers[task] = handler
}
}
task.resume()
send(task, .resumed)
return task
}
// MARK: URLSessionDelegate
func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
send(dataTask, .receivedResponse(response: response))
guard let handler = handlers[dataTask] else {
completionHandler(.cancel)
return
}
if let error = validate(response) {
handler.completion(error)
completionHandler(.cancel)
return
}
completionHandler(.allow)
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
assert(task is URLSessionDataTask)
if let dataTask = task as? URLSessionDataTask {
send(dataTask, .completed(error: error))
}
guard let handler = handlers[task] else {
return
}
handlers[task] = nil
handler.completion(error)
}
// MARK: URLSessionDataDelegate
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
send(dataTask, .receivedData(data: data))
guard let handler = handlers[dataTask], let response = dataTask.response else {
return
}
// Don't store data anywhere, just send it to the pipeline.
handler.didReceiveData(data, response)
}
// MARK: Internal
private func send(_ dataTask: URLSessionDataTask, _ event: DataTaskEvent) {
observer?.dataTask(dataTask, didReceiveEvent: event)
}
private final class _Handler {
let didReceiveData: (Data, URLResponse) -> Void
let completion: (Error?) -> Void
init(didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) {
self.didReceiveData = didReceiveData
self.completion = completion
}
}
}
// MARK: - DataLoaderObserving
public enum DataTaskEvent {
case resumed
case receivedResponse(response: URLResponse)
case receivedData(data: Data)
case completed(error: Error?)
}
/// Allows you to tap into internal events of the data loader. Events are
/// delivered on the internal serial operation queue.
public protocol DataLoaderObserving {
func dataLoader(_ loader: DataLoader, urlSession: URLSession, dataTask: URLSessionDataTask, didReceiveEvent event: DataTaskEvent)
}
protocol _DataLoaderObserving: class {
func dataTask(_ dataTask: URLSessionDataTask, didReceiveEvent event: DataTaskEvent)
}