ImageDownloader.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  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. typealias DownloadResult = Result<ImageLoadingResult, KingfisherError>
  32. /// Represents a success result of an image downloading progress.
  33. public struct ImageLoadingResult {
  34. /// The downloaded image.
  35. public let image: KFCrossPlatformImage
  36. /// Original URL of the image request.
  37. public let url: URL?
  38. /// The raw data received from downloader.
  39. public let originalData: Data
  40. }
  41. /// Represents a task of an image downloading process.
  42. public struct DownloadTask {
  43. /// The `SessionDataTask` object bounded to this download task. Multiple `DownloadTask`s could refer
  44. /// to a same `sessionTask`. This is an optimization in Kingfisher to prevent multiple downloading task
  45. /// for the same URL resource at the same time.
  46. ///
  47. /// When you `cancel` a `DownloadTask`, this `SessionDataTask` and its cancel token will be pass through.
  48. /// You can use them to identify the cancelled task.
  49. public let sessionTask: SessionDataTask
  50. /// The cancel token which is used to cancel the task. This is only for identify the task when it is cancelled.
  51. /// To cancel a `DownloadTask`, use `cancel` instead.
  52. public let cancelToken: SessionDataTask.CancelToken
  53. /// Cancel this task if it is running. It will do nothing if this task is not running.
  54. ///
  55. /// - Note:
  56. /// In Kingfisher, there is an optimization to prevent starting another download task if the target URL is being
  57. /// downloading. However, even when internally no new session task created, a `DownloadTask` will be still created
  58. /// and returned when you call related methods, but it will share the session downloading task with a previous task.
  59. /// In this case, if multiple `DownloadTask`s share a single session download task, cancelling a `DownloadTask`
  60. /// does not affect other `DownloadTask`s.
  61. ///
  62. /// If you need to cancel all `DownloadTask`s of a url, use `ImageDownloader.cancel(url:)`. If you need to cancel
  63. /// all downloading tasks of an `ImageDownloader`, use `ImageDownloader.cancelAll()`.
  64. public func cancel() {
  65. sessionTask.cancel(token: cancelToken)
  66. }
  67. }
  68. extension DownloadTask {
  69. enum WrappedTask {
  70. case download(DownloadTask)
  71. case dataProviding
  72. func cancel() {
  73. switch self {
  74. case .download(let task): task.cancel()
  75. case .dataProviding: break
  76. }
  77. }
  78. var value: DownloadTask? {
  79. switch self {
  80. case .download(let task): return task
  81. case .dataProviding: return nil
  82. }
  83. }
  84. }
  85. }
  86. /// Represents a downloading manager for requesting the image with a URL from server.
  87. open class ImageDownloader {
  88. // MARK: Singleton
  89. /// The default downloader.
  90. public static let `default` = ImageDownloader(name: "default")
  91. // MARK: Public Properties
  92. /// The duration before the downloading is timeout. Default is 15 seconds.
  93. open var downloadTimeout: TimeInterval = 15.0
  94. /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this
  95. /// set will be ignored. You can use this set to specify the self-signed site. It only will be used if you don't
  96. /// specify the `authenticationChallengeResponder`.
  97. ///
  98. /// If `authenticationChallengeResponder` is set, this property will be ignored and the implementation of
  99. /// `authenticationChallengeResponder` will be used instead.
  100. open var trustedHosts: Set<String>?
  101. /// Use this to set supply a configuration for the downloader. By default,
  102. /// NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used.
  103. ///
  104. /// You could change the configuration before a downloading task starts.
  105. /// A configuration without persistent storage for caches is requested for downloader working correctly.
  106. open var sessionConfiguration = URLSessionConfiguration.ephemeral {
  107. didSet {
  108. session.invalidateAndCancel()
  109. session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
  110. }
  111. }
  112. /// Whether the download requests should use pipeline or not. Default is false.
  113. open var requestsUsePipelining = false
  114. /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
  115. open weak var delegate: ImageDownloaderDelegate?
  116. /// A responder for authentication challenge.
  117. /// Downloader will forward the received authentication challenge for the downloading session to this responder.
  118. open weak var authenticationChallengeResponder: AuthenticationChallengeResponsable?
  119. private let name: String
  120. private let sessionDelegate: SessionDelegate
  121. private var session: URLSession
  122. // MARK: Initializers
  123. /// Creates a downloader with name.
  124. ///
  125. /// - Parameter name: The name for the downloader. It should not be empty.
  126. public init(name: String) {
  127. if name.isEmpty {
  128. fatalError("[Kingfisher] You should specify a name for the downloader. "
  129. + "A downloader with empty name is not permitted.")
  130. }
  131. self.name = name
  132. sessionDelegate = SessionDelegate()
  133. session = URLSession(
  134. configuration: sessionConfiguration,
  135. delegate: sessionDelegate,
  136. delegateQueue: nil)
  137. authenticationChallengeResponder = self
  138. setupSessionHandler()
  139. }
  140. deinit { session.invalidateAndCancel() }
  141. private func setupSessionHandler() {
  142. sessionDelegate.onReceiveSessionChallenge.delegate(on: self) { (self, invoke) in
  143. self.authenticationChallengeResponder?.downloader(self, didReceive: invoke.1, completionHandler: invoke.2)
  144. }
  145. sessionDelegate.onReceiveSessionTaskChallenge.delegate(on: self) { (self, invoke) in
  146. self.authenticationChallengeResponder?.downloader(
  147. self, task: invoke.1, didReceive: invoke.2, completionHandler: invoke.3)
  148. }
  149. sessionDelegate.onValidStatusCode.delegate(on: self) { (self, code) in
  150. return (self.delegate ?? self).isValidStatusCode(code, for: self)
  151. }
  152. sessionDelegate.onDownloadingFinished.delegate(on: self) { (self, value) in
  153. let (url, result) = value
  154. do {
  155. let value = try result.get()
  156. self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: value, error: nil)
  157. } catch {
  158. self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: nil, error: error)
  159. }
  160. }
  161. sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in
  162. guard let url = task.originalURL else {
  163. return task.mutableData
  164. }
  165. return (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, for: url)
  166. }
  167. }
  168. // Wraps `completionHandler` to `onCompleted` respectively.
  169. private func createCompletionCallBack(_ completionHandler: ((DownloadResult) -> Void)?) -> Delegate<DownloadResult, Void>? {
  170. return completionHandler.map { block -> Delegate<DownloadResult, Void> in
  171. let delegate = Delegate<Result<ImageLoadingResult, KingfisherError>, Void>()
  172. delegate.delegate(on: self) { (self, callback) in
  173. block(callback)
  174. }
  175. return delegate
  176. }
  177. }
  178. private func createTaskCallback(
  179. _ completionHandler: ((DownloadResult) -> Void)?,
  180. options: KingfisherParsedOptionsInfo
  181. ) -> SessionDataTask.TaskCallback
  182. {
  183. return SessionDataTask.TaskCallback(
  184. onCompleted: createCompletionCallBack(completionHandler),
  185. options: options
  186. )
  187. }
  188. private func createDownloadContext(
  189. with url: URL,
  190. options: KingfisherParsedOptionsInfo,
  191. done: @escaping ((Result<DownloadingContext, KingfisherError>) -> Void)
  192. )
  193. {
  194. func checkRequestAndDone(r: URLRequest) {
  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 = r.url, !url.absoluteString.isEmpty else {
  198. done(.failure(KingfisherError.requestError(reason: .invalidURL(request: r))))
  199. return
  200. }
  201. done(.success(DownloadingContext(url: url, request: r, options: options)))
  202. }
  203. // Creates default request.
  204. var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout)
  205. request.httpShouldUsePipelining = requestsUsePipelining
  206. if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) , options.lowDataModeSource != nil {
  207. request.allowsConstrainedNetworkAccess = false
  208. }
  209. if let requestModifier = options.requestModifier {
  210. // Modifies request before sending.
  211. requestModifier.modified(for: request) { result in
  212. guard let finalRequest = result else {
  213. done(.failure(KingfisherError.requestError(reason: .emptyRequest)))
  214. return
  215. }
  216. checkRequestAndDone(r: finalRequest)
  217. }
  218. } else {
  219. checkRequestAndDone(r: request)
  220. }
  221. }
  222. private func addDownloadTask(
  223. context: DownloadingContext,
  224. callback: SessionDataTask.TaskCallback
  225. ) -> DownloadTask
  226. {
  227. // Ready to start download. Add it to session task manager (`sessionHandler`)
  228. let downloadTask: DownloadTask
  229. if let existingTask = sessionDelegate.task(for: context.url) {
  230. downloadTask = sessionDelegate.append(existingTask, url: context.url, callback: callback)
  231. } else {
  232. let sessionDataTask = session.dataTask(with: context.request)
  233. sessionDataTask.priority = context.options.downloadPriority
  234. downloadTask = sessionDelegate.add(sessionDataTask, url: context.url, callback: callback)
  235. }
  236. return downloadTask
  237. }
  238. private func reportWillDownloadImage(url: URL, request: URLRequest) {
  239. delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
  240. }
  241. private func reportDidDownloadImageData(result: Result<(Data, URLResponse?), KingfisherError>, url: URL) {
  242. var response: URLResponse?
  243. var err: Error?
  244. do {
  245. response = try result.get().1
  246. } catch {
  247. err = error
  248. }
  249. self.delegate?.imageDownloader(
  250. self,
  251. didFinishDownloadingImageForURL: url,
  252. with: response,
  253. error: err
  254. )
  255. }
  256. private func reportDidProcessImage(
  257. result: Result<KFCrossPlatformImage, KingfisherError>, url: URL, response: URLResponse?
  258. )
  259. {
  260. if let image = try? result.get() {
  261. self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response)
  262. }
  263. }
  264. private func startDownloadTask(
  265. context: DownloadingContext,
  266. callback: SessionDataTask.TaskCallback
  267. ) -> DownloadTask
  268. {
  269. let downloadTask = addDownloadTask(context: context, callback: callback)
  270. let sessionTask = downloadTask.sessionTask
  271. guard !sessionTask.started else {
  272. return downloadTask
  273. }
  274. sessionTask.onTaskDone.delegate(on: self) { (self, done) in
  275. // Underlying downloading finishes.
  276. // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback]
  277. let (result, callbacks) = done
  278. // Before processing the downloaded data.
  279. self.reportDidDownloadImageData(result: result, url: context.url)
  280. switch result {
  281. // Download finished. Now process the data to an image.
  282. case .success(let (data, response)):
  283. let processor = ImageDataProcessor(
  284. data: data, callbacks: callbacks, processingQueue: context.options.processingQueue
  285. )
  286. processor.onImageProcessed.delegate(on: self) { (self, done) in
  287. // `onImageProcessed` will be called for `callbacks.count` times, with each
  288. // `SessionDataTask.TaskCallback` as the input parameter.
  289. // result: Result<Image>, callback: SessionDataTask.TaskCallback
  290. let (result, callback) = done
  291. self.reportDidProcessImage(result: result, url: context.url, response: response)
  292. let imageResult = result.map { ImageLoadingResult(image: $0, url: context.url, originalData: data) }
  293. let queue = callback.options.callbackQueue
  294. queue.execute { callback.onCompleted?.call(imageResult) }
  295. }
  296. processor.process()
  297. case .failure(let error):
  298. callbacks.forEach { callback in
  299. let queue = callback.options.callbackQueue
  300. queue.execute { callback.onCompleted?.call(.failure(error)) }
  301. }
  302. }
  303. }
  304. reportWillDownloadImage(url: context.url, request: context.request)
  305. sessionTask.resume()
  306. return downloadTask
  307. }
  308. // MARK: Downloading Task
  309. /// Downloads an image with a URL and option. Invoked internally by Kingfisher. Subclasses must invoke super.
  310. ///
  311. /// - Parameters:
  312. /// - url: Target URL.
  313. /// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
  314. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue
  315. /// defined in `.callbackQueue` in `options` parameter.
  316. /// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
  317. @discardableResult
  318. open func downloadImage(
  319. with url: URL,
  320. options: KingfisherParsedOptionsInfo,
  321. completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  322. {
  323. var downloadTask: DownloadTask?
  324. createDownloadContext(with: url, options: options) { result in
  325. switch result {
  326. case .success(let context):
  327. // `downloadTask` will be set if the downloading started immediately. This is the case when no request
  328. // modifier or a sync modifier (`ImageDownloadRequestModifier`) is used. Otherwise, when an
  329. // `AsyncImageDownloadRequestModifier` is used the returned `downloadTask` of this method will be `nil`
  330. // and the actual "delayed" task is given in `AsyncImageDownloadRequestModifier.onDownloadTaskStarted`
  331. // callback.
  332. downloadTask = self.startDownloadTask(
  333. context: context,
  334. callback: self.createTaskCallback(completionHandler, options: options)
  335. )
  336. if let modifier = options.requestModifier {
  337. modifier.onDownloadTaskStarted?(downloadTask)
  338. }
  339. case .failure(let error):
  340. options.callbackQueue.execute {
  341. completionHandler?(.failure(error))
  342. }
  343. }
  344. }
  345. return downloadTask
  346. }
  347. /// Downloads an image with a URL and option.
  348. ///
  349. /// - Parameters:
  350. /// - url: Target URL.
  351. /// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
  352. /// - progressBlock: Called when the download progress updated. This block will be always be called in main queue.
  353. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue
  354. /// defined in `.callbackQueue` in `options` parameter.
  355. /// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
  356. @discardableResult
  357. open func downloadImage(
  358. with url: URL,
  359. options: KingfisherOptionsInfo? = nil,
  360. progressBlock: DownloadProgressBlock? = nil,
  361. completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  362. {
  363. var info = KingfisherParsedOptionsInfo(options)
  364. if let block = progressBlock {
  365. info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
  366. }
  367. return downloadImage(
  368. with: url,
  369. options: info,
  370. completionHandler: completionHandler)
  371. }
  372. /// Downloads an image with a URL and option.
  373. ///
  374. /// - Parameters:
  375. /// - url: Target URL.
  376. /// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
  377. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue
  378. /// defined in `.callbackQueue` in `options` parameter.
  379. /// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
  380. @discardableResult
  381. open func downloadImage(
  382. with url: URL,
  383. options: KingfisherOptionsInfo? = nil,
  384. completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  385. {
  386. downloadImage(
  387. with: url,
  388. options: KingfisherParsedOptionsInfo(options),
  389. completionHandler: completionHandler
  390. )
  391. }
  392. }
  393. // MARK: Cancelling Task
  394. extension ImageDownloader {
  395. /// Cancel all downloading tasks for this `ImageDownloader`. It will trigger the completion handlers
  396. /// for all not-yet-finished downloading tasks.
  397. ///
  398. /// If you need to only cancel a certain task, call `cancel()` on the `DownloadTask`
  399. /// returned by the downloading methods. If you need to cancel all `DownloadTask`s of a certain url,
  400. /// use `ImageDownloader.cancel(url:)`.
  401. public func cancelAll() {
  402. sessionDelegate.cancelAll()
  403. }
  404. /// Cancel all downloading tasks for a given URL. It will trigger the completion handlers for
  405. /// all not-yet-finished downloading tasks for the URL.
  406. ///
  407. /// - Parameter url: The URL which you want to cancel downloading.
  408. public func cancel(url: URL) {
  409. sessionDelegate.cancel(url: url)
  410. }
  411. }
  412. // Use the default implementation from extension of `AuthenticationChallengeResponsable`.
  413. extension ImageDownloader: AuthenticationChallengeResponsable {}
  414. // Use the default implementation from extension of `ImageDownloaderDelegate`.
  415. extension ImageDownloader: ImageDownloaderDelegate {}
  416. extension ImageDownloader {
  417. struct DownloadingContext {
  418. let url: URL
  419. let request: URLRequest
  420. let options: KingfisherParsedOptionsInfo
  421. }
  422. }