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

2 years ago
  1. // The MIT License (MIT)
  2. //
  3. // Copyright (c) 2015-2021 Alexander Grebenyuk (github.com/kean).
  4. import Foundation
  5. public protocol Cancellable: AnyObject {
  6. func cancel()
  7. }
  8. public protocol DataLoading {
  9. /// - parameter didReceiveData: Can be called multiple times if streaming
  10. /// is supported.
  11. /// - parameter completion: Must be called once after all (or none in case
  12. /// of an error) `didReceiveData` closures have been called.
  13. func loadData(with request: URLRequest,
  14. didReceiveData: @escaping (Data, URLResponse) -> Void,
  15. completion: @escaping (Error?) -> Void) -> Cancellable
  16. /// Removes data for the given request.
  17. func removeData(for request: URLRequest)
  18. }
  19. extension URLSessionTask: Cancellable {}
  20. /// Provides basic networking using `URLSession`.
  21. public final class DataLoader: DataLoading, _DataLoaderObserving {
  22. public let session: URLSession
  23. private let impl = _DataLoader()
  24. public var observer: DataLoaderObserving?
  25. weak var pipeline: ImagePipeline?
  26. deinit {
  27. session.invalidateAndCancel()
  28. #if TRACK_ALLOCATIONS
  29. Allocations.decrement("DataLoader")
  30. #endif
  31. }
  32. /// Initializes `DataLoader` with the given configuration.
  33. /// - parameter configuration: `URLSessionConfiguration.default` with
  34. /// `URLCache` with 0 MB memory capacity and 150 MB disk capacity.
  35. public init(configuration: URLSessionConfiguration = DataLoader.defaultConfiguration,
  36. validate: @escaping (URLResponse) -> Swift.Error? = DataLoader.validate) {
  37. let queue = OperationQueue()
  38. queue.maxConcurrentOperationCount = 1
  39. self.session = URLSession(configuration: configuration, delegate: impl, delegateQueue: queue)
  40. self.impl.validate = validate
  41. self.impl.observer = self
  42. #if TRACK_ALLOCATIONS
  43. Allocations.increment("DataLoader")
  44. #endif
  45. }
  46. // Performance optimization to reduce number of context switches.
  47. func attach(pipeline: ImagePipeline) {
  48. self.pipeline = pipeline
  49. self.session.delegateQueue.underlyingQueue = pipeline.queue
  50. }
  51. /// Returns a default configuration which has a `sharedUrlCache` set
  52. /// as a `urlCache`.
  53. public static var defaultConfiguration: URLSessionConfiguration {
  54. let conf = URLSessionConfiguration.default
  55. conf.urlCache = DataLoader.sharedUrlCache
  56. return conf
  57. }
  58. /// Validates `HTTP` responses by checking that the status code is 2xx. If
  59. /// it's not returns `DataLoader.Error.statusCodeUnacceptable`.
  60. public static func validate(response: URLResponse) -> Swift.Error? {
  61. guard let response = response as? HTTPURLResponse else {
  62. return nil
  63. }
  64. return (200..<300).contains(response.statusCode) ? nil : Error.statusCodeUnacceptable(response.statusCode)
  65. }
  66. #if !os(macOS) && !targetEnvironment(macCatalyst)
  67. private static let cachePath = "com.github.kean.Nuke.Cache"
  68. #else
  69. private static let cachePath: String = {
  70. let cachePaths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)
  71. if let cachePath = cachePaths.first, let identifier = Bundle.main.bundleIdentifier {
  72. return cachePath.appending("/" + identifier)
  73. }
  74. return ""
  75. }()
  76. #endif
  77. /// Shared url cached used by a default `DataLoader`. The cache is
  78. /// initialized with 0 MB memory capacity and 150 MB disk capacity.
  79. public static let sharedUrlCache: URLCache = {
  80. let diskCapacity = 150 * 1024 * 1024 // 150 MB
  81. #if targetEnvironment(macCatalyst)
  82. return URLCache(memoryCapacity: 0, diskCapacity: diskCapacity, directory: URL(fileURLWithPath: cachePath))
  83. #else
  84. return URLCache(memoryCapacity: 0, diskCapacity: diskCapacity, diskPath: cachePath)
  85. #endif
  86. }()
  87. public func loadData(with request: URLRequest,
  88. didReceiveData: @escaping (Data, URLResponse) -> Void,
  89. completion: @escaping (Swift.Error?) -> Void) -> Cancellable {
  90. return loadData(with: request, isConfined: false, didReceiveData: didReceiveData, completion: completion)
  91. }
  92. func loadData(with request: URLRequest,
  93. isConfined: Bool,
  94. didReceiveData: @escaping (Data, URLResponse) -> Void,
  95. completion: @escaping (Swift.Error?) -> Void) -> Cancellable {
  96. return impl.loadData(with: request, session: session, isConfined: isConfined, didReceiveData: didReceiveData, completion: completion)
  97. }
  98. public func removeData(for request: URLRequest) {
  99. session.configuration.urlCache?.removeCachedResponse(for: request)
  100. }
  101. /// Errors produced by `DataLoader`.
  102. public enum Error: Swift.Error, CustomDebugStringConvertible {
  103. /// Validation failed.
  104. case statusCodeUnacceptable(Int)
  105. public var debugDescription: String {
  106. switch self {
  107. case let .statusCodeUnacceptable(code):
  108. return "Response status code was unacceptable: \(code.description)"
  109. }
  110. }
  111. }
  112. // MARK: _DataLoaderObserving
  113. func dataTask(_ dataTask: URLSessionDataTask, didReceiveEvent event: DataTaskEvent) {
  114. observer?.dataLoader(self, urlSession: session, dataTask: dataTask, didReceiveEvent: event)
  115. }
  116. }
  117. // Actual data loader implementation. Hide NSObject inheritance, hide
  118. // URLSessionDataDelegate conformance, and break retain cycle between URLSession
  119. // and URLSessionDataDelegate.
  120. private final class _DataLoader: NSObject, URLSessionDataDelegate {
  121. var validate: (URLResponse) -> Swift.Error? = DataLoader.validate
  122. private var handlers = [URLSessionTask: _Handler]()
  123. weak var observer: _DataLoaderObserving?
  124. /// Loads data with the given request.
  125. func loadData(with request: URLRequest,
  126. session: URLSession,
  127. isConfined: Bool,
  128. didReceiveData: @escaping (Data, URLResponse) -> Void,
  129. completion: @escaping (Error?) -> Void) -> Cancellable {
  130. let task = session.dataTask(with: request)
  131. let handler = _Handler(didReceiveData: didReceiveData, completion: completion)
  132. if isConfined {
  133. handlers[task] = handler
  134. } else {
  135. session.delegateQueue.addOperation { // `URLSession` is configured to use this same queue
  136. self.handlers[task] = handler
  137. }
  138. }
  139. task.resume()
  140. send(task, .resumed)
  141. return task
  142. }
  143. // MARK: URLSessionDelegate
  144. func urlSession(_ session: URLSession,
  145. dataTask: URLSessionDataTask,
  146. didReceive response: URLResponse,
  147. completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
  148. send(dataTask, .receivedResponse(response: response))
  149. guard let handler = handlers[dataTask] else {
  150. completionHandler(.cancel)
  151. return
  152. }
  153. if let error = validate(response) {
  154. handler.completion(error)
  155. completionHandler(.cancel)
  156. return
  157. }
  158. completionHandler(.allow)
  159. }
  160. func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
  161. assert(task is URLSessionDataTask)
  162. if let dataTask = task as? URLSessionDataTask {
  163. send(dataTask, .completed(error: error))
  164. }
  165. guard let handler = handlers[task] else {
  166. return
  167. }
  168. handlers[task] = nil
  169. handler.completion(error)
  170. }
  171. // MARK: URLSessionDataDelegate
  172. func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
  173. send(dataTask, .receivedData(data: data))
  174. guard let handler = handlers[dataTask], let response = dataTask.response else {
  175. return
  176. }
  177. // Don't store data anywhere, just send it to the pipeline.
  178. handler.didReceiveData(data, response)
  179. }
  180. // MARK: Internal
  181. private func send(_ dataTask: URLSessionDataTask, _ event: DataTaskEvent) {
  182. observer?.dataTask(dataTask, didReceiveEvent: event)
  183. }
  184. private final class _Handler {
  185. let didReceiveData: (Data, URLResponse) -> Void
  186. let completion: (Error?) -> Void
  187. init(didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) {
  188. self.didReceiveData = didReceiveData
  189. self.completion = completion
  190. }
  191. }
  192. }
  193. // MARK: - DataLoaderObserving
  194. public enum DataTaskEvent {
  195. case resumed
  196. case receivedResponse(response: URLResponse)
  197. case receivedData(data: Data)
  198. case completed(error: Error?)
  199. }
  200. /// Allows you to tap into internal events of the data loader. Events are
  201. /// delivered on the internal serial operation queue.
  202. public protocol DataLoaderObserving {
  203. func dataLoader(_ loader: DataLoader, urlSession: URLSession, dataTask: URLSessionDataTask, didReceiveEvent event: DataTaskEvent)
  204. }
  205. protocol _DataLoaderObserving: class {
  206. func dataTask(_ dataTask: URLSessionDataTask, didReceiveEvent event: DataTaskEvent)
  207. }