ImageDownloader.swift 27 KB

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