ImageDownloader.swift 27 KB

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