You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
677 lines
30 KiB
677 lines
30 KiB
// |
|
// 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 {} |
|
|
|
|