ImageDownloader.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  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 successful result of an image downloading process.
  33. public struct ImageLoadingResult {
  34. /// The downloaded image.
  35. public let image: KFCrossPlatformImage
  36. /// The original URL of the image request.
  37. public let url: URL?
  38. /// The raw data received from the downloader.
  39. public let originalData: Data
  40. /// Creates an `ImageDownloadResult` object.
  41. ///
  42. /// - Parameters:
  43. /// - image: The image of the download result.
  44. /// - url: The URL from which the image was downloaded.
  45. /// - originalData: The binary data of the image.
  46. public init(image: KFCrossPlatformImage, url: URL? = nil, originalData: Data) {
  47. self.image = image
  48. self.url = url
  49. self.originalData = originalData
  50. }
  51. }
  52. /// Represents a task in the image downloading process.
  53. ///
  54. /// When a download starts in Kingfisher, the involved methods always return you an instance of ``DownloadTask``. If you
  55. /// need to cancel the task during the download process, you can keep a reference to the instance and call ``cancel()``
  56. /// on it.
  57. public class DownloadTask {
  58. init(sessionTask: SessionDataTask, cancelToken: SessionDataTask.CancelToken) {
  59. self.sessionTask = sessionTask
  60. self.cancelToken = cancelToken
  61. }
  62. /// The ``SessionDataTask`` object associated with this download task. Multiple `DownloadTask`s could refer to the
  63. /// same `sessionTask`. This is an optimization in Kingfisher to prevent multiple downloading tasks for the same
  64. /// URL resource simultaneously.
  65. ///
  66. /// When you call ``DownloadTask/cancel()``, this ``SessionDataTask`` and its cancellation token will be passed
  67. /// along. You can use them to identify the cancelled task.
  68. public let sessionTask: SessionDataTask
  69. /// The cancellation token used to cancel the task.
  70. ///
  71. /// This is solely for identifying the task when it is cancelled. To cancel a ``DownloadTask``, call
  72. /// ``DownloadTask/cancelToken``.
  73. public let cancelToken: SessionDataTask.CancelToken
  74. /// Cancel this single download task if it is running.
  75. ///
  76. /// This method will do nothing if this task is not running.
  77. ///
  78. /// In Kingfisher, there is an optimization to prevent starting another download task if the target URL is currently
  79. /// being downloaded. However, even when internally no new session task is created, a ``DownloadTask`` will still
  80. /// be created and returned when you call related methods. It will share the session downloading task with a
  81. /// previous task.
  82. ///
  83. /// In this case, if multiple ``DownloadTask``s share a single session download task, calling this method
  84. /// does not cancel the actual download process, since there are other `DownloadTask`s need it. It only removes
  85. /// `self` from the download list.
  86. ///
  87. /// > Tip: If you need to cancel all on-going ``DownloadTask``s of a certain URL, use
  88. /// ``ImageDownloader/cancel(url:)``. If you need to cancel all downloading tasks of an ``ImageDownloader``,
  89. /// use ``ImageDownloader/cancelAll()``.
  90. public func cancel() {
  91. sessionTask.cancel(token: cancelToken)
  92. }
  93. }
  94. actor CancellationDownloadTask {
  95. var task: DownloadTask?
  96. func setTask(_ task: DownloadTask?) {
  97. self.task = task
  98. }
  99. }
  100. extension DownloadTask {
  101. enum WrappedTask {
  102. case download(DownloadTask)
  103. case dataProviding
  104. func cancel() {
  105. switch self {
  106. case .download(let task): task.cancel()
  107. case .dataProviding: break
  108. }
  109. }
  110. var value: DownloadTask? {
  111. switch self {
  112. case .download(let task): return task
  113. case .dataProviding: return nil
  114. }
  115. }
  116. }
  117. }
  118. /// Represents a download manager for requesting an image with a URL from the server.
  119. open class ImageDownloader {
  120. // MARK: Singleton
  121. /// The default downloader.
  122. public static let `default` = ImageDownloader(name: "default")
  123. // MARK: Public Properties
  124. /// The duration before the download times out.
  125. ///
  126. /// If the download does not complete before this duration, the URL session will raise a timeout error, which
  127. /// Kingfisher wraps and forwards as a ``KingfisherError/ResponseErrorReason/URLSessionError(error:)``.
  128. ///
  129. /// The default timeout is set to 15 seconds.
  130. open var downloadTimeout: TimeInterval = 15.0
  131. /// A set of trusted hosts when receiving server trust challenges.
  132. ///
  133. /// A challenge with host name contained in this set will be ignored. You can use this set to specify the
  134. /// self-signed site. It only will be used if you don't specify the
  135. /// ``ImageDownloader/authenticationChallengeResponder``.
  136. ///
  137. /// > If ``ImageDownloader/authenticationChallengeResponder`` is set, this property will be ignored and the
  138. /// implementation of ``ImageDownloader/authenticationChallengeResponder`` will be used instead.
  139. open var trustedHosts: Set<String>?
  140. /// Use this to supply a configuration for the downloader.
  141. ///
  142. /// By default, `URLSessionConfiguration.ephemeral` will be used.
  143. ///
  144. /// You can modify the configuration before a downloading task begins. A configuration without persistent storage
  145. /// for caches is necessary for the downloader to function correctly.
  146. ///
  147. /// > Setting a new session delegate to the downloader will invalidate the existing session and create a new one
  148. /// > with the new value and the ``sessionDelegate``.
  149. open var sessionConfiguration = URLSessionConfiguration.ephemeral {
  150. didSet {
  151. session.invalidateAndCancel()
  152. session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
  153. }
  154. }
  155. /// The session delegate which is used to handle the session related tasks.
  156. ///
  157. /// > Setting a new session delegate to the downloader will invalidate the existing session and create a new one
  158. /// > with the new value and the ``sessionConfiguration``.
  159. open var sessionDelegate: SessionDelegate {
  160. didSet {
  161. session.invalidateAndCancel()
  162. session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
  163. setupSessionHandler()
  164. }
  165. }
  166. /// Whether the download requests should use pipeline or not.
  167. ///
  168. /// It sets the `httpShouldUsePipelining` of the `URLRequest` for the download task. Default is false.
  169. open var requestsUsePipelining = false
  170. /// The delegate of this `ImageDownloader` object.
  171. ///
  172. /// See the ``ImageDownloaderDelegate`` protocol for more information.
  173. open weak var delegate: ImageDownloaderDelegate?
  174. /// A responder for authentication challenges.
  175. ///
  176. /// The downloader forwards the received authentication challenge for the downloading session to this responder.
  177. /// See ``AuthenticationChallengeResponsible`` for more.
  178. open weak var authenticationChallengeResponder: AuthenticationChallengeResponsible?
  179. // The downloader name.
  180. private let name: String
  181. // The session bound to the downloader.
  182. private var session: URLSession
  183. // MARK: Initializers
  184. /// Creates a downloader with a given name.
  185. ///
  186. /// - Parameter name: The name for the downloader. It should not be empty.
  187. public init(name: String) {
  188. if name.isEmpty {
  189. fatalError("[Kingfisher] You should specify a name for the downloader. "
  190. + "A downloader with empty name is not permitted.")
  191. }
  192. self.name = name
  193. sessionDelegate = SessionDelegate()
  194. session = URLSession(
  195. configuration: sessionConfiguration,
  196. delegate: sessionDelegate,
  197. delegateQueue: nil)
  198. authenticationChallengeResponder = self
  199. setupSessionHandler()
  200. }
  201. deinit { session.invalidateAndCancel() }
  202. private func setupSessionHandler() {
  203. sessionDelegate.onReceiveSessionChallenge.delegate(on: self) { (self, invoke) in
  204. self.authenticationChallengeResponder?.downloader(self, didReceive: invoke.1, completionHandler: invoke.2)
  205. }
  206. sessionDelegate.onReceiveSessionTaskChallenge.delegate(on: self) { (self, invoke) in
  207. self.authenticationChallengeResponder?.downloader(
  208. self, task: invoke.1, didReceive: invoke.2, completionHandler: invoke.3)
  209. }
  210. sessionDelegate.onValidStatusCode.delegate(on: self) { (self, code) in
  211. (self.delegate ?? self).isValidStatusCode(code, for: self)
  212. }
  213. sessionDelegate.onResponseReceived.delegate(on: self) { (self, response) in
  214. await (self.delegate ?? self).imageDownloader(self, didReceive: response)
  215. }
  216. sessionDelegate.onDownloadingFinished.delegate(on: self) { (self, value) in
  217. let (url, result) = value
  218. do {
  219. let value = try result.get()
  220. self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: value, error: nil)
  221. } catch {
  222. self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: nil, error: error)
  223. }
  224. }
  225. sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in
  226. (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, with: task)
  227. }
  228. }
  229. // Wraps `completionHandler` to `onCompleted` respectively.
  230. private func createCompletionCallBack(_ completionHandler: ((DownloadResult) -> Void)?) -> Delegate<DownloadResult, Void>? {
  231. completionHandler.map { block -> Delegate<DownloadResult, Void> in
  232. let delegate = Delegate<Result<ImageLoadingResult, KingfisherError>, Void>()
  233. delegate.delegate(on: self) { (self, callback) in
  234. block(callback)
  235. }
  236. return delegate
  237. }
  238. }
  239. private func createTaskCallback(
  240. _ completionHandler: ((DownloadResult) -> Void)?,
  241. options: KingfisherParsedOptionsInfo
  242. ) -> SessionDataTask.TaskCallback
  243. {
  244. SessionDataTask.TaskCallback(
  245. onCompleted: createCompletionCallBack(completionHandler),
  246. options: options
  247. )
  248. }
  249. private func createDownloadContext(
  250. with url: URL,
  251. options: KingfisherParsedOptionsInfo,
  252. done: @escaping ((Result<DownloadingContext, KingfisherError>) -> Void)
  253. )
  254. {
  255. @Sendable func checkRequestAndDone(r: URLRequest) {
  256. // There is a possibility that request modifier changed the url to `nil` or empty.
  257. // In this case, throw an error.
  258. guard let url = r.url, !url.absoluteString.isEmpty else {
  259. done(.failure(KingfisherError.requestError(reason: .invalidURL(request: r))))
  260. return
  261. }
  262. done(.success(DownloadingContext(url: url, request: r, options: options)))
  263. }
  264. // Creates default request.
  265. var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout)
  266. request.httpShouldUsePipelining = requestsUsePipelining
  267. if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) , options.lowDataModeSource != nil {
  268. request.allowsConstrainedNetworkAccess = false
  269. }
  270. guard let requestModifier = options.requestModifier else {
  271. checkRequestAndDone(r: request)
  272. return
  273. }
  274. // Modifies request before sending.
  275. // FIXME: A temporary solution for keep the sync `ImageDownloadRequestModifier` behavior as before.
  276. // We should be able to combine two cases once the full async support can be introduced to Kingfisher.
  277. if let m = requestModifier as? ImageDownloadRequestModifier {
  278. guard let result = m.modified(for: request) else {
  279. done(.failure(KingfisherError.requestError(reason: .emptyRequest)))
  280. return
  281. }
  282. checkRequestAndDone(r: result)
  283. } else {
  284. Task { [request] in
  285. guard let result = await requestModifier.modified(for: request) else {
  286. done(.failure(KingfisherError.requestError(reason: .emptyRequest)))
  287. return
  288. }
  289. checkRequestAndDone(r: result)
  290. }
  291. }
  292. }
  293. private func addDownloadTask(
  294. context: DownloadingContext,
  295. callback: SessionDataTask.TaskCallback
  296. ) -> DownloadTask
  297. {
  298. // Ready to start download. Add it to session task manager (`sessionHandler`)
  299. let downloadTask: DownloadTask
  300. if let existingTask = sessionDelegate.task(for: context.url) {
  301. downloadTask = sessionDelegate.append(existingTask, callback: callback)
  302. } else {
  303. let sessionDataTask = session.dataTask(with: context.request)
  304. sessionDataTask.priority = context.options.downloadPriority
  305. downloadTask = sessionDelegate.add(sessionDataTask, url: context.url, callback: callback)
  306. }
  307. return downloadTask
  308. }
  309. private func reportWillDownloadImage(url: URL, request: URLRequest) {
  310. delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
  311. }
  312. private func reportDidDownloadImageData(result: Result<(Data, URLResponse?), KingfisherError>, url: URL) {
  313. var response: URLResponse?
  314. var err: Error?
  315. do {
  316. response = try result.get().1
  317. } catch {
  318. err = error
  319. }
  320. self.delegate?.imageDownloader(
  321. self,
  322. didFinishDownloadingImageForURL: url,
  323. with: response,
  324. error: err
  325. )
  326. }
  327. private func reportDidProcessImage(
  328. result: Result<KFCrossPlatformImage, KingfisherError>, url: URL, response: URLResponse?
  329. )
  330. {
  331. if let image = try? result.get() {
  332. self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response)
  333. }
  334. }
  335. private func startDownloadTask(
  336. context: DownloadingContext,
  337. callback: SessionDataTask.TaskCallback
  338. ) -> DownloadTask
  339. {
  340. let downloadTask = addDownloadTask(context: context, callback: callback)
  341. let sessionTask = downloadTask.sessionTask
  342. guard !sessionTask.started else {
  343. return downloadTask
  344. }
  345. sessionTask.onTaskDone.delegate(on: self) { (self, done) in
  346. // Underlying downloading finishes.
  347. // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback]
  348. let (result, callbacks) = done
  349. // Before processing the downloaded data.
  350. self.reportDidDownloadImageData(result: result, url: context.url)
  351. switch result {
  352. // Download finished. Now process the data to an image.
  353. case .success(let (data, response)):
  354. let processor = ImageDataProcessor(
  355. data: data, callbacks: callbacks, processingQueue: context.options.processingQueue
  356. )
  357. processor.onImageProcessed.delegate(on: self) { (self, done) in
  358. // `onImageProcessed` will be called for `callbacks.count` times, with each
  359. // `SessionDataTask.TaskCallback` as the input parameter.
  360. // result: Result<Image>, callback: SessionDataTask.TaskCallback
  361. let (result, callback) = done
  362. self.reportDidProcessImage(result: result, url: context.url, response: response)
  363. let imageResult = result.map { ImageLoadingResult(image: $0, url: context.url, originalData: data) }
  364. let queue = callback.options.callbackQueue
  365. queue.execute { callback.onCompleted?.call(imageResult) }
  366. }
  367. processor.process()
  368. case .failure(let error):
  369. callbacks.forEach { callback in
  370. let queue = callback.options.callbackQueue
  371. queue.execute { callback.onCompleted?.call(.failure(error)) }
  372. }
  373. }
  374. }
  375. reportWillDownloadImage(url: context.url, request: context.request)
  376. sessionTask.resume()
  377. return downloadTask
  378. }
  379. // MARK: Downloading Task
  380. /// Downloads an image with a URL and options.
  381. ///
  382. /// - Parameters:
  383. /// - url: The target URL.
  384. /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``.
  385. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue
  386. /// defined in ``KingfisherOptionsInfoItem/callbackQueue(_:)`` in the `options` parameter.
  387. ///
  388. /// - Returns: A downloading task. You can call ``DownloadTask/cancelToken`` on it to stop the download task.
  389. @discardableResult
  390. open func downloadImage(
  391. with url: URL,
  392. options: KingfisherParsedOptionsInfo,
  393. completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  394. {
  395. var downloadTask: DownloadTask?
  396. createDownloadContext(with: url, options: options) { result in
  397. switch result {
  398. case .success(let context):
  399. // `downloadTask` will be set if the downloading started immediately. This is the case when no request
  400. // modifier or a sync modifier (`ImageDownloadRequestModifier`) is used. Otherwise, when an
  401. // `AsyncImageDownloadRequestModifier` is used the returned `downloadTask` of this method will be `nil`
  402. // and the actual "delayed" task is given in `AsyncImageDownloadRequestModifier.onDownloadTaskStarted`
  403. // callback.
  404. downloadTask = self.startDownloadTask(
  405. context: context,
  406. callback: self.createTaskCallback(completionHandler, options: options)
  407. )
  408. if let modifier = options.requestModifier {
  409. modifier.onDownloadTaskStarted?(downloadTask)
  410. }
  411. case .failure(let error):
  412. options.callbackQueue.execute {
  413. completionHandler?(.failure(error))
  414. }
  415. }
  416. }
  417. return downloadTask
  418. }
  419. /// Downloads an image with a URL and options.
  420. ///
  421. /// - Parameters:
  422. /// - url: The target URL.
  423. /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``.
  424. /// - progressBlock: Called when the download progress is updated. This block will always be called on the main
  425. /// queue.
  426. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue
  427. /// defined in ``KingfisherOptionsInfoItem/callbackQueue(_:)`` in the `options` parameter.
  428. ///
  429. /// - Returns: A downloading task. You can call ``DownloadTask/cancelToken`` on it to stop the download task.
  430. @discardableResult
  431. open func downloadImage(
  432. with url: URL,
  433. options: KingfisherOptionsInfo? = nil,
  434. progressBlock: DownloadProgressBlock? = nil,
  435. completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  436. {
  437. var info = KingfisherParsedOptionsInfo(options)
  438. if let block = progressBlock {
  439. info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
  440. }
  441. return downloadImage(
  442. with: url,
  443. options: info,
  444. completionHandler: completionHandler)
  445. }
  446. /// Downloads an image with a URL and options.
  447. ///
  448. /// - Parameters:
  449. /// - url: The target URL.
  450. /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``.
  451. /// - completionHandler: Called when the download progress finishes. This block will be called in the queue
  452. /// defined in ``KingfisherOptionsInfoItem/callbackQueue(_:)`` in the `options` parameter.
  453. ///
  454. /// - Returns: A downloading task. You can call ``DownloadTask/cancelToken`` on it to stop the download task.
  455. @discardableResult
  456. open func downloadImage(
  457. with url: URL,
  458. options: KingfisherOptionsInfo? = nil,
  459. completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
  460. {
  461. downloadImage(
  462. with: url,
  463. options: KingfisherParsedOptionsInfo(options),
  464. completionHandler: completionHandler
  465. )
  466. }
  467. }
  468. // Concurrency
  469. extension ImageDownloader {
  470. /// Downloads an image with a URL and option.
  471. ///
  472. /// - Parameters:
  473. /// - url: Target URL.
  474. /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``.
  475. /// - Returns: The image loading result.
  476. ///
  477. /// > To cancel the download task initialized by this method, cancel the `Task` where this method is running in.
  478. public func downloadImage(
  479. with url: URL,
  480. options: KingfisherParsedOptionsInfo
  481. ) async throws -> ImageLoadingResult {
  482. let task = CancellationDownloadTask()
  483. return try await withTaskCancellationHandler {
  484. try await withCheckedThrowingContinuation { continuation in
  485. let downloadTask = downloadImage(with: url, options: options) { result in
  486. continuation.resume(with: result)
  487. }
  488. if Task.isCancelled {
  489. downloadTask?.cancel()
  490. } else {
  491. Task {
  492. await task.setTask(downloadTask)
  493. }
  494. }
  495. }
  496. } onCancel: {
  497. Task {
  498. await task.task?.cancel()
  499. }
  500. }
  501. }
  502. /// Downloads an image with a URL and option.
  503. ///
  504. /// - Parameters:
  505. /// - url: Target URL.
  506. /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``.
  507. /// - progressBlock: Called when the download progress updated. This block will be always be called in main queue.
  508. /// - Returns: The image loading result.
  509. ///
  510. /// > To cancel the download task initialized by this method, cancel the `Task` where this method is running in.
  511. public func downloadImage(
  512. with url: URL,
  513. options: KingfisherOptionsInfo? = nil,
  514. progressBlock: DownloadProgressBlock? = nil
  515. ) async throws -> ImageLoadingResult
  516. {
  517. var info = KingfisherParsedOptionsInfo(options)
  518. if let block = progressBlock {
  519. info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
  520. }
  521. return try await downloadImage(with: url, options: info)
  522. }
  523. /// Downloads an image with a URL and option.
  524. ///
  525. /// - Parameters:
  526. /// - url: Target URL.
  527. /// - options: The options that can control download behavior. See ``KingfisherOptionsInfo``.
  528. /// - Returns: The image loading result.
  529. ///
  530. /// > To cancel the download task initialized by this method, cancel the `Task` where this method is running in.
  531. public func downloadImage(
  532. with url: URL,
  533. options: KingfisherOptionsInfo? = nil
  534. ) async throws -> ImageLoadingResult
  535. {
  536. try await downloadImage(with: url, options: KingfisherParsedOptionsInfo(options))
  537. }
  538. }
  539. // MARK: Cancelling Task
  540. extension ImageDownloader {
  541. /// Cancel all downloading tasks for this ``ImageDownloader``.
  542. ///
  543. /// It will trigger the completion handlers for all not-yet-finished downloading tasks with a cancellation error.
  544. ///
  545. /// If you need to only cancel a certain task, call ``DownloadTask/cancel()`` on the task returned by the
  546. /// downloading methods. If you need to cancel all ``DownloadTask``s of a certain URL, use
  547. /// ``ImageDownloader/cancel(url:)``.
  548. public func cancelAll() {
  549. sessionDelegate.cancelAll()
  550. }
  551. /// Cancel all downloading tasks for a given URL.
  552. ///
  553. /// It will trigger the completion handlers for all not-yet-finished downloading tasks for the URL with a
  554. /// cancellation error.
  555. ///
  556. /// - Parameter url: The URL for which you want to cancel downloading.
  557. public func cancel(url: URL) {
  558. sessionDelegate.cancel(url: url)
  559. }
  560. }
  561. // Use the default implementation from extension of `AuthenticationChallengeResponsible`.
  562. extension ImageDownloader: AuthenticationChallengeResponsible {}
  563. // Use the default implementation from extension of `ImageDownloaderDelegate`.
  564. extension ImageDownloader: ImageDownloaderDelegate {}
  565. extension ImageDownloader {
  566. struct DownloadingContext {
  567. let url: URL
  568. let request: URLRequest
  569. let options: KingfisherParsedOptionsInfo
  570. }
  571. }