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.

377 lines
16 KiB

  1. //
  2. // ImageDownloader.swift
  3. // Kingfisher
  4. //
  5. // Created by Wei Wang on 15/4/6.
  6. //
  7. // Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. #if os(macOS)
  27. import AppKit
  28. #else
  29. import UIKit
  30. #endif
  31. /// Represents a success result of an image downloading progress.
  32. public struct ImageLoadingResult {
  33. /// The downloaded image.
  34. public let image: KFCrossPlatformImage
  35. /// Original URL of the image request.
  36. public let url: URL?
  37. /// The raw data received from downloader.
  38. public let originalData: Data
  39. }
  40. /// Represents a task of an image downloading process.
  41. public struct DownloadTask {
  42. /// The `SessionDataTask` object bounded to this download task. Multiple `DownloadTask`s could refer
  43. /// to a same `sessionTask`. This is an optimization in Kingfisher to prevent multiple downloading task
  44. /// for the same URL resource at the same time.
  45. ///
  46. /// When you `cancel` a `DownloadTask`, this `SessionDataTask` and its cancel token will be pass through.
  47. /// You can use them to identify the cancelled task.
  48. public let sessionTask: SessionDataTask
  49. /// The cancel token which is used to cancel the task. This is only for identify the task when it is cancelled.
  50. /// To cancel a `DownloadTask`, use `cancel` instead.
  51. public let cancelToken: SessionDataTask.CancelToken
  52. /// Cancel this task if it is running. It will do nothing if this task is not running.
  53. ///
  54. /// - Note:
  55. /// In Kingfisher, there is an optimization to prevent starting another download task if the target URL is being
  56. /// downloading. However, even when internally no new session task created, a `DownloadTask` will be still created
  57. /// and returned when you call related methods, but it will share the session downloading task with a previous task.
  58. /// In this case, if multiple `DownloadTask`s share a single session download task, cancelling a `DownloadTask`
  59. /// does not affect other `DownloadTask`s.
  60. ///
  61. /// If you need to cancel all `DownloadTask`s of a url, use `ImageDownloader.cancel(url:)`. If you need to cancel
  62. /// all downloading tasks of an `ImageDownloader`, use `ImageDownloader.cancelAll()`.
  63. public func cancel() {
  64. sessionTask.cancel(token: cancelToken)
  65. }
  66. }
  67. extension DownloadTask {
  68. enum WrappedTask {
  69. case download(DownloadTask)
  70. case dataProviding
  71. func cancel() {
  72. switch self {
  73. case .download(let task): task.cancel()
  74. case .dataProviding: break
  75. }
  76. }
  77. var value: DownloadTask? {
  78. switch self {
  79. case .download(let task): return task
  80. case .dataProviding: return nil
  81. }
  82. }
  83. }
  84. }
  85. /// Represents a downloading manager for requesting the image with a URL from server.
  86. open class ImageDownloader {
  87. // MARK: Singleton
  88. /// The default downloader.
  89. public static let `default` = ImageDownloader(name: "default")
  90. // MARK: Public Properties
  91. /// The duration before the downloading is timeout. Default is 15 seconds.
  92. open var downloadTimeout: TimeInterval = 15.0
  93. /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this
  94. /// set will be ignored. You can use this set to specify the self-signed site. It only will be used if you don't
  95. /// specify the `authenticationChallengeResponder`.
  96. ///
  97. /// If `authenticationChallengeResponder` is set, this property will be ignored and the implementation of
  98. /// `authenticationChallengeResponder` will be used instead.
  99. open var trustedHosts: Set<String>?
  100. /// Use this to set supply a configuration for the downloader. By default,
  101. /// NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used.
  102. ///
  103. /// You could change the configuration before a downloading task starts.
  104. /// A configuration without persistent storage for caches is requested for downloader working correctly.
  105. open var sessionConfiguration = URLSessionConfiguration.ephemeral {
  106. didSet {
  107. session.invalidateAndCancel()
  108. session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
  109. }
  110. }
  111. /// Whether the download requests should use pipeline or not. Default is false.
  112. open var requestsUsePipelining = false
  113. /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
  114. open weak var delegate: ImageDownloaderDelegate?
  115. /// A responder for authentication challenge.
  116. /// Downloader will forward the received authentication challenge for the downloading session to this responder.
  117. open weak var authenticationChallengeResponder: AuthenticationChallengeResponsable?
  118. private let name: String
  119. private let sessionDelegate: SessionDelegate
  120. private var session: URLSession
  121. // MARK: Initializers
  122. /// Creates a downloader with name.
  123. ///
  124. /// - Parameter name: The name for the downloader. It should not be empty.
  125. public init(name: String) {
  126. if name.isEmpty {
  127. fatalError("[Kingfisher] You should specify a name for the downloader. "
  128. + "A downloader with empty name is not permitted.")
  129. }
  130. self.name = name
  131. sessionDelegate = SessionDelegate()
  132. session = URLSession(
  133. configuration: sessionConfiguration,
  134. delegate: sessionDelegate,
  135. delegateQueue: nil)
  136. authenticationChallengeResponder = self
  137. setupSessionHandler()
  138. }
  139. deinit { session.invalidateAndCancel() }
  140. private func setupSessionHandler() {
  141. sessionDelegate.onReceiveSessionChallenge.delegate(on: self) { (self, invoke) in
  142. self.authenticationChallengeResponder?.downloader(self, didReceive: invoke.1, completionHandler: invoke.2)
  143. }
  144. sessionDelegate.onReceiveSessionTaskChallenge.delegate(on: self) { (self, invoke) in
  145. self.authenticationChallengeResponder?.downloader(
  146. self, task: invoke.1, didReceive: invoke.2, completionHandler: invoke.3)
  147. }
  148. sessionDelegate.onValidStatusCode.delegate(on: self) { (self, code) in
  149. return (self.delegate ?? self).isValidStatusCode(code, for: self)
  150. }
  151. sessionDelegate.onDownloadingFinished.delegate(on: self) { (self, value) in
  152. let (url, result) = value
  153. do {
  154. let value = try result.get()
  155. self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: value, error: nil)
  156. } catch {
  157. self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: nil, error: error)
  158. }
  159. }
  160. sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in
  161. guard let url = task.task.originalRequest?.url else {
  162. return task.mutableData
  163. }
  164. return (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, for: url)
  165. }
  166. }
  167. // MARK: Dowloading Task
  168. /// Downloads an image with a URL and option. Invoked internally by Kingfisher. Subclasses must invoke super.
  169. ///
  170. /// - Parameters:
  171. /// - url: Target URL.
  172. /// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
  173. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue
  174. /// defined in `.callbackQueue` in `options` parameter.
  175. /// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
  176. @discardableResult
  177. open func downloadImage(
  178. with url: URL,
  179. options: KingfisherParsedOptionsInfo,
  180. completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  181. {
  182. // Creates default request.
  183. var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout)
  184. request.httpShouldUsePipelining = requestsUsePipelining
  185. if let requestModifier = options.requestModifier {
  186. // Modifies request before sending.
  187. guard let r = requestModifier.modified(for: request) else {
  188. options.callbackQueue.execute {
  189. completionHandler?(.failure(KingfisherError.requestError(reason: .emptyRequest)))
  190. }
  191. return nil
  192. }
  193. request = r
  194. }
  195. // There is a possibility that request modifier changed the url to `nil` or empty.
  196. // In this case, throw an error.
  197. guard let url = request.url, !url.absoluteString.isEmpty else {
  198. options.callbackQueue.execute {
  199. completionHandler?(.failure(KingfisherError.requestError(reason: .invalidURL(request: request))))
  200. }
  201. return nil
  202. }
  203. // Wraps `completionHandler` to `onCompleted` respectively.
  204. let onCompleted = completionHandler.map {
  205. block -> Delegate<Result<ImageLoadingResult, KingfisherError>, Void> in
  206. let delegate = Delegate<Result<ImageLoadingResult, KingfisherError>, Void>()
  207. delegate.delegate(on: self) { (_, callback) in
  208. block(callback)
  209. }
  210. return delegate
  211. }
  212. // SessionDataTask.TaskCallback is a wrapper for `onCompleted` and `options` (for processor info)
  213. let callback = SessionDataTask.TaskCallback(
  214. onCompleted: onCompleted,
  215. options: options
  216. )
  217. // Ready to start download. Add it to session task manager (`sessionHandler`)
  218. let downloadTask: DownloadTask
  219. if let existingTask = sessionDelegate.task(for: url) {
  220. downloadTask = sessionDelegate.append(existingTask, url: url, callback: callback)
  221. } else {
  222. let sessionDataTask = session.dataTask(with: request)
  223. sessionDataTask.priority = options.downloadPriority
  224. downloadTask = sessionDelegate.add(sessionDataTask, url: url, callback: callback)
  225. }
  226. let sessionTask = downloadTask.sessionTask
  227. // Start the session task if not started yet.
  228. if !sessionTask.started {
  229. sessionTask.onTaskDone.delegate(on: self) { (self, done) in
  230. // Underlying downloading finishes.
  231. // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback]
  232. let (result, callbacks) = done
  233. // Before processing the downloaded data.
  234. do {
  235. let value = try result.get()
  236. self.delegate?.imageDownloader(
  237. self,
  238. didFinishDownloadingImageForURL: url,
  239. with: value.1,
  240. error: nil
  241. )
  242. } catch {
  243. self.delegate?.imageDownloader(
  244. self,
  245. didFinishDownloadingImageForURL: url,
  246. with: nil,
  247. error: error
  248. )
  249. }
  250. switch result {
  251. // Download finished. Now process the data to an image.
  252. case .success(let (data, response)):
  253. let processor = ImageDataProcessor(
  254. data: data, callbacks: callbacks, processingQueue: options.processingQueue)
  255. processor.onImageProcessed.delegate(on: self) { (self, result) in
  256. // `onImageProcessed` will be called for `callbacks.count` times, with each
  257. // `SessionDataTask.TaskCallback` as the input parameter.
  258. // result: Result<Image>, callback: SessionDataTask.TaskCallback
  259. let (result, callback) = result
  260. if let image = try? result.get() {
  261. self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response)
  262. }
  263. let imageResult = result.map { ImageLoadingResult(image: $0, url: url, originalData: data) }
  264. let queue = callback.options.callbackQueue
  265. queue.execute { callback.onCompleted?.call(imageResult) }
  266. }
  267. processor.process()
  268. case .failure(let error):
  269. callbacks.forEach { callback in
  270. let queue = callback.options.callbackQueue
  271. queue.execute { callback.onCompleted?.call(.failure(error)) }
  272. }
  273. }
  274. }
  275. delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
  276. sessionTask.resume()
  277. }
  278. return downloadTask
  279. }
  280. /// Downloads an image with a URL and option.
  281. ///
  282. /// - Parameters:
  283. /// - url: Target URL.
  284. /// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
  285. /// - progressBlock: Called when the download progress updated. This block will be always be called in main queue.
  286. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue
  287. /// defined in `.callbackQueue` in `options` parameter.
  288. /// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
  289. @discardableResult
  290. open func downloadImage(
  291. with url: URL,
  292. options: KingfisherOptionsInfo? = nil,
  293. progressBlock: DownloadProgressBlock? = nil,
  294. completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  295. {
  296. var info = KingfisherParsedOptionsInfo(options)
  297. if let block = progressBlock {
  298. info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
  299. }
  300. return downloadImage(
  301. with: url,
  302. options: info,
  303. completionHandler: completionHandler)
  304. }
  305. }
  306. // MARK: Cancelling Task
  307. extension ImageDownloader {
  308. /// Cancel all downloading tasks for this `ImageDownloader`. It will trigger the completion handlers
  309. /// for all not-yet-finished downloading tasks.
  310. ///
  311. /// If you need to only cancel a certain task, call `cancel()` on the `DownloadTask`
  312. /// returned by the downloading methods. If you need to cancel all `DownloadTask`s of a certain url,
  313. /// use `ImageDownloader.cancel(url:)`.
  314. public func cancelAll() {
  315. sessionDelegate.cancelAll()
  316. }
  317. /// Cancel all downloading tasks for a given URL. It will trigger the completion handlers for
  318. /// all not-yet-finished downloading tasks for the URL.
  319. ///
  320. /// - Parameter url: The URL which you want to cancel downloading.
  321. public func cancel(url: URL) {
  322. sessionDelegate.cancel(url: url)
  323. }
  324. }
  325. // Use the default implementation from extension of `AuthenticationChallengeResponsable`.
  326. extension ImageDownloader: AuthenticationChallengeResponsable {}
  327. // Use the default implementation from extension of `ImageDownloaderDelegate`.
  328. extension ImageDownloader: ImageDownloaderDelegate {}