123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677 |
- //
- // ImageDownloader.swift
- // Kingfisher
- //
- // Created by Wei Wang on 15/4/6.
- //
- // Copyright (c) 2018 Wei Wang <onevcat@gmail.com>
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
- #if os(macOS)
- import AppKit
- #else
- import UIKit
- #endif
- /// Progress update block of downloader.
- public typealias ImageDownloaderProgressBlock = DownloadProgressBlock
- /// Completion block of downloader.
- public typealias ImageDownloaderCompletionHandler = ((_ image: Image?, _ error: NSError?, _ url: URL?, _ originalData: Data?) -> Void)
- /// Download task.
- public struct RetrieveImageDownloadTask {
- let internalTask: URLSessionDataTask
-
- /// Downloader by which this task is initialized.
- public private(set) weak var ownerDownloader: ImageDownloader?
-
- /// Cancel this download task. It will trigger the completion handler with an NSURLErrorCancelled error.
- /// If you want to cancel all downloading tasks, call `cancelAll()` of `ImageDownloader` instance.
- public func cancel() {
- ownerDownloader?.cancel(self)
- }
-
- /// The original request URL of this download task.
- public var url: URL? {
- return internalTask.originalRequest?.url
- }
-
- /// The relative priority of this download task.
- /// It represents the `priority` property of the internal `NSURLSessionTask` of this download task.
- /// The value for it is between 0.0~1.0. Default priority is value of 0.5.
- /// See documentation on `priority` of `NSURLSessionTask` for more about it.
- public var priority: Float {
- get {
- return internalTask.priority
- }
- set {
- internalTask.priority = newValue
- }
- }
- }
- ///The code of errors which `ImageDownloader` might encountered.
- public enum KingfisherError: Int {
-
- /// badData: The downloaded data is not an image or the data is corrupted.
- case badData = 10000
-
- /// notModified: The remote server responded a 304 code. No image data downloaded.
- case notModified = 10001
-
- /// The HTTP status code in response is not valid. If an invalid
- /// code error received, you could check the value under `KingfisherErrorStatusCodeKey`
- /// in `userInfo` to see the code.
- case invalidStatusCode = 10002
-
- /// notCached: The image requested is not in cache but .onlyFromCache is activated.
- case notCached = 10003
-
- /// The URL is invalid.
- case invalidURL = 20000
-
- /// The downloading task is cancelled before started.
- case downloadCancelledBeforeStarting = 30000
- }
- /// Key will be used in the `userInfo` of `.invalidStatusCode`
- public let KingfisherErrorStatusCodeKey = "statusCode"
- /// Protocol of `ImageDownloader`.
- public protocol ImageDownloaderDelegate: AnyObject {
- /**
- Called when the `ImageDownloader` object will start downloading an image from specified URL.
-
- - parameter downloader: The `ImageDownloader` object finishes the downloading.
- - parameter url: URL of the original request URL.
- - parameter response: The request object for the download process.
- */
- func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?)
-
- /**
- Called when the `ImageDownloader` completes a downloading request with success or failure.
-
- - parameter downloader: The `ImageDownloader` object finishes the downloading.
- - parameter url: URL of the original request URL.
- - parameter response: The response object of the downloading process.
- - parameter error: The error in case of failure.
- */
- func imageDownloader(_ downloader: ImageDownloader, didFinishDownloadingImageForURL url: URL, with response: URLResponse?, error: Error?)
-
- /**
- Called when the `ImageDownloader` object successfully downloaded an image from specified URL.
-
- - parameter downloader: The `ImageDownloader` object finishes the downloading.
- - parameter image: Downloaded image.
- - parameter url: URL of the original request URL.
- - parameter response: The response object of the downloading process.
- */
- func imageDownloader(_ downloader: ImageDownloader, didDownload image: Image, for url: URL, with response: URLResponse?)
-
- /**
- Check if a received HTTP status code is valid or not.
- By default, a status code between 200 to 400 (excluded) is considered as valid.
- If an invalid code is received, the downloader will raise an .invalidStatusCode error.
- It has a `userInfo` which includes this statusCode and localizedString error message.
-
- - parameter code: The received HTTP status code.
- - parameter downloader: The `ImageDownloader` object asking for validate status code.
-
- - returns: Whether this HTTP status code is valid or not.
-
- - Note: If the default 200 to 400 valid code does not suit your need,
- you can implement this method to change that behavior.
- */
- func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool
-
- /**
- Called when the `ImageDownloader` object successfully downloaded image data from specified URL.
-
- - parameter downloader: The `ImageDownloader` object finishes data downloading.
- - parameter data: Downloaded data.
- - parameter url: URL of the original request URL.
-
- - returns: The data from which Kingfisher should use to create an image.
-
- - Note: This callback can be used to preprocess raw image data
- before creation of UIImage instance (i.e. decrypting or verification).
- */
- func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data?
- }
- extension ImageDownloaderDelegate {
-
- public func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?) {}
-
- public func imageDownloader(_ downloader: ImageDownloader, didFinishDownloadingImageForURL url: URL, with response: URLResponse?, error: Error?) {}
-
- public func imageDownloader(_ downloader: ImageDownloader, didDownload image: Image, for url: URL, with response: URLResponse?) {}
-
- public func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool {
- return (200..<400).contains(code)
- }
- public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? {
- return data
- }
- }
- /// Protocol indicates that an authentication challenge could be handled.
- public protocol AuthenticationChallengeResponsable: AnyObject {
- /**
- Called when an session level authentication challenge is received.
- This method provide a chance to handle and response to the authentication challenge before downloading could start.
-
- - parameter downloader: The downloader which receives this challenge.
- - parameter challenge: An object that contains the request for authentication.
- - parameter completionHandler: A handler that your delegate method must call.
-
- - Note: This method is a forward from `URLSessionDelegate.urlSession(:didReceiveChallenge:completionHandler:)`. Please refer to the document of it in `URLSessionDelegate`.
- */
- func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
- /**
- Called when an session level authentication challenge is received.
- This method provide a chance to handle and response to the authentication challenge before downloading could start.
-
- - parameter downloader: The downloader which receives this challenge.
- - parameter task: The task whose request requires authentication.
- - parameter challenge: An object that contains the request for authentication.
- - parameter completionHandler: A handler that your delegate method must call.
-
- - Note: This method is a forward from `URLSessionTaskDelegate.urlSession(:task:didReceiveChallenge:completionHandler:)`. Please refer to the document of it in `URLSessionTaskDelegate`.
- */
- func downloader(_ downloader: ImageDownloader, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
- }
- extension AuthenticationChallengeResponsable {
-
- func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
-
- if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
- if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) {
- let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
- completionHandler(.useCredential, credential)
- return
- }
- }
-
- completionHandler(.performDefaultHandling, nil)
- }
-
- func downloader(_ downloader: ImageDownloader, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
-
- completionHandler(.performDefaultHandling, nil)
- }
- }
- /// `ImageDownloader` represents a downloading manager for requesting the image with a URL from server.
- open class ImageDownloader {
-
- class ImageFetchLoad {
- var contents = [(callback: CallbackPair, options: KingfisherOptionsInfo)]()
- var responseData = NSMutableData()
- var downloadTaskCount = 0
- var downloadTask: RetrieveImageDownloadTask?
- var cancelSemaphore: DispatchSemaphore?
- }
-
- // MARK: - Public property
- /// The duration before the download is timeout. Default is 15 seconds.
- open var downloadTimeout: TimeInterval = 15.0
-
- /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this set will be ignored.
- /// You can use this set to specify the self-signed site. It only will be used if you don't specify the `authenticationChallengeResponder`.
- /// If `authenticationChallengeResponder` is set, this property will be ignored and the implementation of `authenticationChallengeResponder` will be used instead.
- open var trustedHosts: Set<String>?
-
- /// Use this to set supply a configuration for the downloader. By default, NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used.
- /// You could change the configuration before a downloading task starts. A configuration without persistent storage for caches is requested for downloader working correctly.
- open var sessionConfiguration = URLSessionConfiguration.ephemeral {
- didSet {
- session?.invalidateAndCancel()
- session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: nil)
- }
- }
-
- /// Whether the download requests should use pipline or not. Default is false.
- open var requestsUsePipelining = false
-
- fileprivate let sessionHandler: ImageDownloaderSessionHandler
- fileprivate var session: URLSession?
-
- /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
- open weak var delegate: ImageDownloaderDelegate?
-
- /// A responder for authentication challenge.
- /// Downloader will forward the received authentication challenge for the downloading session to this responder.
- open weak var authenticationChallengeResponder: AuthenticationChallengeResponsable?
-
- // MARK: - Internal property
- let barrierQueue: DispatchQueue
- let processQueue: DispatchQueue
- let cancelQueue: DispatchQueue
-
- typealias CallbackPair = (progressBlock: ImageDownloaderProgressBlock?, completionHandler: ImageDownloaderCompletionHandler?)
-
- var fetchLoads = [URL: ImageFetchLoad]()
-
- // MARK: - Public method
- /// The default downloader.
- public static let `default` = ImageDownloader(name: "default")
-
- /**
- Init a downloader with name.
-
- - parameter name: The name for the downloader. It should not be empty.
- */
- public init(name: String) {
- if name.isEmpty {
- fatalError("[Kingfisher] You should specify a name for the downloader. A downloader with empty name is not permitted.")
- }
-
- barrierQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Barrier.\(name)", attributes: .concurrent)
- processQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process.\(name)", attributes: .concurrent)
- cancelQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Cancel.\(name)")
-
- sessionHandler = ImageDownloaderSessionHandler(name: name)
- // Provide a default implement for challenge responder.
- authenticationChallengeResponder = sessionHandler
- session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: .main)
- }
-
- deinit {
- session?.invalidateAndCancel()
- }
-
- func fetchLoad(for url: URL) -> ImageFetchLoad? {
- var fetchLoad: ImageFetchLoad?
- barrierQueue.sync(flags: .barrier) { fetchLoad = fetchLoads[url] }
- return fetchLoad
- }
-
- /**
- Download an image with a URL and option.
-
- - parameter url: Target URL.
- - parameter retrieveImageTask: The task to cooperate with cache. Pass `nil` if you are not trying to use downloader and cache.
- - parameter options: The options could control download behavior. See `KingfisherOptionsInfo`.
- - parameter progressBlock: Called when the download progress updated.
- - parameter completionHandler: Called when the download progress finishes.
-
- - returns: A downloading task. You could call `cancel` on it to stop the downloading process.
- */
- @discardableResult
- open func downloadImage(with url: URL,
- retrieveImageTask: RetrieveImageTask? = nil,
- options: KingfisherOptionsInfo? = nil,
- progressBlock: ImageDownloaderProgressBlock? = nil,
- completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask?
- {
- if let retrieveImageTask = retrieveImageTask, retrieveImageTask.cancelledBeforeDownloadStarting {
- completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
- return nil
- }
-
- let timeout = self.downloadTimeout == 0.0 ? 15.0 : self.downloadTimeout
-
- // We need to set the URL as the load key. So before setup progress, we need to ask the `requestModifier` for a final URL.
- var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: timeout)
- request.httpShouldUsePipelining = requestsUsePipelining
- if let modifier = options?.modifier {
- guard let r = modifier.modified(for: request) else {
- completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
- return nil
- }
- request = r
- }
-
- // There is a possibility that request modifier changed the url to `nil` or empty.
- guard let url = request.url, !url.absoluteString.isEmpty else {
- completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.invalidURL.rawValue, userInfo: nil), nil, nil)
- return nil
- }
-
- var downloadTask: RetrieveImageDownloadTask?
- setup(progressBlock: progressBlock, with: completionHandler, for: url, options: options) {(session, fetchLoad) -> Void in
- if fetchLoad.downloadTask == nil {
- let dataTask = session.dataTask(with: request)
-
- fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self)
-
- dataTask.priority = options?.downloadPriority ?? URLSessionTask.defaultPriority
- self.delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
- dataTask.resume()
-
- // Hold self while the task is executing.
- self.sessionHandler.downloadHolder = self
- }
-
- fetchLoad.downloadTaskCount += 1
- downloadTask = fetchLoad.downloadTask
-
- retrieveImageTask?.downloadTask = downloadTask
- }
- return downloadTask
- }
-
- }
- // MARK: - Download method
- extension ImageDownloader {
-
- // A single key may have multiple callbacks. Only download once.
- func setup(progressBlock: ImageDownloaderProgressBlock?, with completionHandler: ImageDownloaderCompletionHandler?, for url: URL, options: KingfisherOptionsInfo?, started: @escaping ((URLSession, ImageFetchLoad) -> Void)) {
- func prepareFetchLoad() {
- barrierQueue.sync(flags: .barrier) {
- let loadObjectForURL = fetchLoads[url] ?? ImageFetchLoad()
- let callbackPair = (progressBlock: progressBlock, completionHandler: completionHandler)
-
- loadObjectForURL.contents.append((callbackPair, options ?? KingfisherEmptyOptionsInfo))
-
- fetchLoads[url] = loadObjectForURL
-
- if let session = session {
- started(session, loadObjectForURL)
- }
- }
- }
-
- if let fetchLoad = fetchLoad(for: url), fetchLoad.downloadTaskCount == 0 {
- if fetchLoad.cancelSemaphore == nil {
- fetchLoad.cancelSemaphore = DispatchSemaphore(value: 0)
- }
- cancelQueue.async {
- _ = fetchLoad.cancelSemaphore?.wait(timeout: .distantFuture)
- fetchLoad.cancelSemaphore = nil
- prepareFetchLoad()
- }
- } else {
- prepareFetchLoad()
- }
- }
-
- private func cancelTaskImpl(_ task: RetrieveImageDownloadTask, fetchLoad: ImageFetchLoad? = nil, ignoreTaskCount: Bool = false) {
-
- func getFetchLoad(from task: RetrieveImageDownloadTask) -> ImageFetchLoad? {
- guard let URL = task.internalTask.originalRequest?.url,
- let imageFetchLoad = self.fetchLoads[URL] else
- {
- return nil
- }
- return imageFetchLoad
- }
-
- guard let imageFetchLoad = fetchLoad ?? getFetchLoad(from: task) else {
- return
- }
- imageFetchLoad.downloadTaskCount -= 1
- if ignoreTaskCount || imageFetchLoad.downloadTaskCount == 0 {
- task.internalTask.cancel()
- }
- }
-
- func cancel(_ task: RetrieveImageDownloadTask) {
- barrierQueue.sync(flags: .barrier) { cancelTaskImpl(task) }
- }
-
- /// Cancel all downloading tasks. It will trigger the completion handlers for all not-yet-finished
- /// downloading tasks with an NSURLErrorCancelled error.
- ///
- /// If you need to only cancel a certain task, call `cancel()` on the `RetrieveImageDownloadTask`
- /// returned by the downloading methods.
- public func cancelAll() {
- barrierQueue.sync(flags: .barrier) {
- fetchLoads.forEach { v in
- let fetchLoad = v.value
- guard let task = fetchLoad.downloadTask else { return }
- cancelTaskImpl(task, fetchLoad: fetchLoad, ignoreTaskCount: true)
- }
- }
- }
- }
- // MARK: - NSURLSessionDataDelegate
- /// Delegate class for `NSURLSessionTaskDelegate`.
- /// The session object will hold its delegate until it gets invalidated.
- /// If we use `ImageDownloader` as the session delegate, it will not be released.
- /// So we need an additional handler to break the retain cycle.
- // See https://github.com/onevcat/Kingfisher/issues/235
- final class ImageDownloaderSessionHandler: NSObject, URLSessionDataDelegate, AuthenticationChallengeResponsable {
- private let downloaderQueue: DispatchQueue
- // The holder will keep downloader not released while a data task is being executed.
- // It will be set when the task started, and reset when the task finished.
- private var _downloadHolder: ImageDownloader?
- var downloadHolder: ImageDownloader? {
- get {
- return downloaderQueue.sync { _downloadHolder }
- }
- set {
- downloaderQueue.sync { _downloadHolder = newValue }
- }
- }
- init(name: String) {
- downloaderQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.SessionHandler.\(name)")
- super.init()
- }
-
- func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
-
- guard let downloader = downloadHolder else {
- completionHandler(.cancel)
- return
- }
-
- var disposition = URLSession.ResponseDisposition.allow
-
- if let statusCode = (response as? HTTPURLResponse)?.statusCode,
- let url = dataTask.originalRequest?.url,
- !(downloader.delegate ?? downloader).isValidStatusCode(statusCode, for: downloader)
- {
- let error = NSError(domain: KingfisherErrorDomain,
- code: KingfisherError.invalidStatusCode.rawValue,
- userInfo: [KingfisherErrorStatusCodeKey: statusCode, NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: statusCode)])
-
- // Needs to be called before callCompletionHandlerFailure() because it removes downloadHolder
- if let downloader = downloadHolder {
- downloader.delegate?.imageDownloader(downloader, didFinishDownloadingImageForURL: url, with: response, error: error)
- }
-
- callCompletionHandlerFailure(error: error, url: url)
- disposition = .cancel
- }
-
- completionHandler(disposition)
- }
-
- func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
- guard let downloader = downloadHolder else {
- return
- }
- if let url = dataTask.originalRequest?.url, let fetchLoad = downloader.fetchLoad(for: url) {
- fetchLoad.responseData.append(data)
-
- if let expectedLength = dataTask.response?.expectedContentLength {
- for content in fetchLoad.contents {
- DispatchQueue.main.async {
- content.callback.progressBlock?(Int64(fetchLoad.responseData.length), expectedLength)
- }
- }
- }
- }
- }
-
- func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
-
- guard let url = task.originalRequest?.url else {
- return
- }
-
- if let downloader = downloadHolder {
- downloader.delegate?.imageDownloader(downloader, didFinishDownloadingImageForURL: url, with: task.response, error: error)
- }
-
- guard error == nil else {
- callCompletionHandlerFailure(error: error!, url: url)
- return
- }
-
- processImage(for: task, url: url)
- }
-
- /**
- This method is exposed since the compiler requests. Do not call it.
- */
- func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
- guard let downloader = downloadHolder else {
- return
- }
-
- downloader.authenticationChallengeResponder?.downloader(downloader, didReceive: challenge, completionHandler: completionHandler)
- }
-
- func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
- guard let downloader = downloadHolder else {
- return
- }
-
- downloader.authenticationChallengeResponder?.downloader(downloader, task: task, didReceive: challenge, completionHandler: completionHandler)
- }
-
- private func cleanFetchLoad(for url: URL) {
- guard let downloader = downloadHolder else {
- return
- }
- downloader.barrierQueue.sync(flags: .barrier) {
- downloader.fetchLoads.removeValue(forKey: url)
- if downloader.fetchLoads.isEmpty {
- downloadHolder = nil
- }
- }
- }
-
- private func callCompletionHandlerFailure(error: Error, url: URL) {
- guard let downloader = downloadHolder, let fetchLoad = downloader.fetchLoad(for: url) else {
- return
- }
-
- // We need to clean the fetch load first, before actually calling completion handler.
- cleanFetchLoad(for: url)
-
- var leftSignal: Int
- repeat {
- leftSignal = fetchLoad.cancelSemaphore?.signal() ?? 0
- } while leftSignal != 0
-
- for content in fetchLoad.contents {
- content.options.callbackDispatchQueue.safeAsync {
- content.callback.completionHandler?(nil, error as NSError, url, nil)
- }
- }
- }
-
- private func processImage(for task: URLSessionTask, url: URL) {
- guard let downloader = downloadHolder else {
- return
- }
-
- // We are on main queue when receiving this.
- downloader.processQueue.async {
-
- guard let fetchLoad = downloader.fetchLoad(for: url) else {
- return
- }
-
- self.cleanFetchLoad(for: url)
-
- let data: Data?
- let fetchedData = fetchLoad.responseData as Data
-
- if let delegate = downloader.delegate {
- data = delegate.imageDownloader(downloader, didDownload: fetchedData, for: url)
- } else {
- data = fetchedData
- }
-
- // Cache the processed images. So we do not need to re-process the image if using the same processor.
- // Key is the identifier of processor.
- var imageCache: [String: Image] = [:]
- for content in fetchLoad.contents {
-
- let options = content.options
- let completionHandler = content.callback.completionHandler
- let callbackQueue = options.callbackDispatchQueue
-
- let processor = options.processor
- var image = imageCache[processor.identifier]
- if let data = data, image == nil {
- image = processor.process(item: .data(data), options: options)
- // Add the processed image to cache.
- // If `image` is nil, nothing will happen (since the key is not existing before).
- imageCache[processor.identifier] = image
- }
-
- if let image = image {
- downloader.delegate?.imageDownloader(downloader, didDownload: image, for: url, with: task.response)
- let imageModifier = options.imageModifier
- let finalImage = imageModifier.modify(image)
- if options.backgroundDecode {
- let decodedImage = finalImage.kf.decoded
- callbackQueue.safeAsync { completionHandler?(decodedImage, nil, url, data) }
- } else {
- callbackQueue.safeAsync { completionHandler?(finalImage, nil, url, data) }
- }
-
- } else {
- if let res = task.response as? HTTPURLResponse , res.statusCode == 304 {
- let notModified = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notModified.rawValue, userInfo: nil)
- completionHandler?(nil, notModified, url, nil)
- continue
- }
-
- let badData = NSError(domain: KingfisherErrorDomain, code: KingfisherError.badData.rawValue, userInfo: nil)
- callbackQueue.safeAsync { completionHandler?(nil, badData, url, nil) }
- }
- }
- }
- }
- }
- // Placeholder. For retrieving extension methods of ImageDownloaderDelegate
- extension ImageDownloader: ImageDownloaderDelegate {}
|