2 // ImageDownloader.swift
5 // Created by Wei Wang on 15/4/6.
7 // Copyright (c) 2018 Wei Wang <onevcat@gmail.com>
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:
16 // The above copyright notice and this permission notice shall be included in
17 // all copies or substantial portions of the Software.
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
33 /// Progress update block of downloader.
34 public typealias ImageDownloaderProgressBlock = DownloadProgressBlock
36 /// Completion block of downloader.
37 public typealias ImageDownloaderCompletionHandler = ((_ image: Image?, _ error: NSError?, _ url: URL?, _ originalData: Data?) -> Void)
40 public struct RetrieveImageDownloadTask {
41 let internalTask: URLSessionDataTask
43 /// Downloader by which this task is intialized.
44 public private(set) weak var ownerDownloader: ImageDownloader?
47 /// Cancel this download task. It will trigger the completion handler with an NSURLErrorCancelled error.
48 /// If you want to cancel all downloading tasks, call `cancelAll()` of `ImageDownloader` instance.
49 public func cancel() {
50 ownerDownloader?.cancel(self)
53 /// The original request URL of this download task.
54 public var url: URL? {
55 return internalTask.originalRequest?.url
58 /// The relative priority of this download task.
59 /// It represents the `priority` property of the internal `NSURLSessionTask` of this download task.
60 /// The value for it is between 0.0~1.0. Default priority is value of 0.5.
61 /// See documentation on `priority` of `NSURLSessionTask` for more about it.
62 public var priority: Float {
64 return internalTask.priority
67 internalTask.priority = newValue
72 ///The code of errors which `ImageDownloader` might encountered.
73 public enum KingfisherError: Int {
75 /// badData: The downloaded data is not an image or the data is corrupted.
78 /// notModified: The remote server responsed a 304 code. No image data downloaded.
79 case notModified = 10001
81 /// The HTTP status code in response is not valid. If an invalid
82 /// code error received, you could check the value under `KingfisherErrorStatusCodeKey`
83 /// in `userInfo` to see the code.
84 case invalidStatusCode = 10002
86 /// notCached: The image rquested is not in cache but .onlyFromCache is activated.
87 case notCached = 10003
89 /// The URL is invalid.
90 case invalidURL = 20000
92 /// The downloading task is cancelled before started.
93 case downloadCancelledBeforeStarting = 30000
96 /// Key will be used in the `userInfo` of `.invalidStatusCode`
97 public let KingfisherErrorStatusCodeKey = "statusCode"
99 /// Protocol of `ImageDownloader`.
100 public protocol ImageDownloaderDelegate: class {
102 Called when the `ImageDownloader` object successfully downloaded an image from specified URL.
104 - parameter downloader: The `ImageDownloader` object finishes the downloading.
105 - parameter image: Downloaded image.
106 - parameter url: URL of the original request URL.
107 - parameter response: The response object of the downloading process.
109 func imageDownloader(_ downloader: ImageDownloader, didDownload image: Image, for url: URL, with response: URLResponse?)
112 Called when the `ImageDownloader` object starts to download an image from specified URL.
114 - parameter downloader: The `ImageDownloader` object starts the downloading.
115 - parameter url: URL of the original request.
116 - parameter response: The request object of the downloading process.
118 func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?)
121 Check if a received HTTP status code is valid or not.
122 By default, a status code between 200 to 400 (excluded) is considered as valid.
123 If an invalid code is received, the downloader will raise an .invalidStatusCode error.
124 It has a `userInfo` which includes this statusCode and localizedString error message.
126 - parameter code: The received HTTP status code.
127 - parameter downloader: The `ImageDownloader` object asking for validate status code.
129 - returns: Whether this HTTP status code is valid or not.
131 - Note: If the default 200 to 400 valid code does not suit your need,
132 you can implement this method to change that behavior.
134 func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool
137 Called when the `ImageDownloader` object successfully downloaded image data from specified URL.
139 - parameter downloader: The `ImageDownloader` object finishes data downloading.
140 - parameter data: Downloaded data.
141 - parameter url: URL of the original request URL.
143 - returns: The data from which Kingfisher should use to create an image.
145 - Note: This callback can be used to preprocess raw image data
146 before creation of UIImage instance (i.e. decrypting or verification).
148 func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data?
151 extension ImageDownloaderDelegate {
152 public func imageDownloader(_ downloader: ImageDownloader, didDownload image: Image, for url: URL, with response: URLResponse?) {}
154 public func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?) {}
155 public func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool {
156 return (200..<400).contains(code)
158 public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? {
163 /// Protocol indicates that an authentication challenge could be handled.
164 public protocol AuthenticationChallengeResponsable: class {
166 Called when an session level authentication challenge is received.
167 This method provide a chance to handle and response to the authentication challenge before downloading could start.
169 - parameter downloader: The downloader which receives this challenge.
170 - parameter challenge: An object that contains the request for authentication.
171 - parameter completionHandler: A handler that your delegate method must call.
173 - Note: This method is a forward from `URLSessionDelegate.urlSession(:didReceiveChallenge:completionHandler:)`. Please refer to the document of it in `URLSessionDelegate`.
175 func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
178 Called when an session level authentication challenge is received.
179 This method provide a chance to handle and response to the authentication challenge before downloading could start.
181 - parameter downloader: The downloader which receives this challenge.
182 - parameter task: The task whose request requires authentication.
183 - parameter challenge: An object that contains the request for authentication.
184 - parameter completionHandler: A handler that your delegate method must call.
186 - Note: This method is a forward from `URLSessionTaskDelegate.urlSession(:task:didReceiveChallenge:completionHandler:)`. Please refer to the document of it in `URLSessionTaskDelegate`.
188 func downloader(_ downloader: ImageDownloader, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
191 extension AuthenticationChallengeResponsable {
193 func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
195 if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
196 if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) {
197 let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
198 completionHandler(.useCredential, credential)
203 completionHandler(.performDefaultHandling, nil)
206 func downloader(_ downloader: ImageDownloader, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
208 completionHandler(.performDefaultHandling, nil)
213 /// `ImageDownloader` represents a downloading manager for requesting the image with a URL from server.
214 open class ImageDownloader {
216 class ImageFetchLoad {
217 var contents = [(callback: CallbackPair, options: KingfisherOptionsInfo)]()
218 var responseData = NSMutableData()
220 var downloadTaskCount = 0
221 var downloadTask: RetrieveImageDownloadTask?
222 var cancelSemaphore: DispatchSemaphore?
225 // MARK: - Public property
226 /// The duration before the download is timeout. Default is 15 seconds.
227 open var downloadTimeout: TimeInterval = 15.0
229 /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this set will be ignored.
230 /// You can use this set to specify the self-signed site. It only will be used if you don't specify the `authenticationChallengeResponder`.
231 /// If `authenticationChallengeResponder` is set, this property will be ignored and the implemention of `authenticationChallengeResponder` will be used instead.
232 open var trustedHosts: Set<String>?
234 /// Use this to set supply a configuration for the downloader. By default, NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used.
235 /// You could change the configuration before a downloaing task starts. A configuration without persistent storage for caches is requsted for downloader working correctly.
236 open var sessionConfiguration = URLSessionConfiguration.ephemeral {
238 session?.invalidateAndCancel()
239 session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: OperationQueue.main)
243 /// Whether the download requests should use pipeling or not. Default is false.
244 open var requestsUsePipelining = false
246 fileprivate let sessionHandler: ImageDownloaderSessionHandler
247 fileprivate var session: URLSession?
249 /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
250 open weak var delegate: ImageDownloaderDelegate?
252 /// A responder for authentication challenge.
253 /// Downloader will forward the received authentication challenge for the downloading session to this responder.
254 open weak var authenticationChallengeResponder: AuthenticationChallengeResponsable?
256 // MARK: - Internal property
257 let barrierQueue: DispatchQueue
258 let processQueue: DispatchQueue
259 let cancelQueue: DispatchQueue
261 typealias CallbackPair = (progressBlock: ImageDownloaderProgressBlock?, completionHandler: ImageDownloaderCompletionHandler?)
263 var fetchLoads = [URL: ImageFetchLoad]()
265 // MARK: - Public method
266 /// The default downloader.
267 public static let `default` = ImageDownloader(name: "default")
270 Init a downloader with name.
272 - parameter name: The name for the downloader. It should not be empty.
274 - returns: The downloader object.
276 public init(name: String) {
278 fatalError("[Kingfisher] You should specify a name for the downloader. A downloader with empty name is not permitted.")
281 barrierQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Barrier.\(name)", attributes: .concurrent)
282 processQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process.\(name)", attributes: .concurrent)
283 cancelQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Cancel.\(name)")
285 sessionHandler = ImageDownloaderSessionHandler()
287 // Provide a default implement for challenge responder.
288 authenticationChallengeResponder = sessionHandler
289 session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: .main)
293 session?.invalidateAndCancel()
296 func fetchLoad(for url: URL) -> ImageFetchLoad? {
297 var fetchLoad: ImageFetchLoad?
298 barrierQueue.sync(flags: .barrier) { fetchLoad = fetchLoads[url] }
303 Download an image with a URL and option.
305 - parameter url: Target URL.
306 - parameter retrieveImageTask: The task to cooporate with cache. Pass `nil` if you are not trying to use downloader and cache.
307 - parameter options: The options could control download behavior. See `KingfisherOptionsInfo`.
308 - parameter progressBlock: Called when the download progress updated.
309 - parameter completionHandler: Called when the download progress finishes.
311 - returns: A downloading task. You could call `cancel` on it to stop the downloading process.
314 open func downloadImage(with url: URL,
315 retrieveImageTask: RetrieveImageTask? = nil,
316 options: KingfisherOptionsInfo? = nil,
317 progressBlock: ImageDownloaderProgressBlock? = nil,
318 completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask?
320 if let retrieveImageTask = retrieveImageTask, retrieveImageTask.cancelledBeforeDownloadStarting {
321 completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
325 let timeout = self.downloadTimeout == 0.0 ? 15.0 : self.downloadTimeout
327 // We need to set the URL as the load key. So before setup progress, we need to ask the `requestModifier` for a final URL.
328 var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: timeout)
329 request.httpShouldUsePipelining = requestsUsePipelining
331 if let modifier = options?.modifier {
332 guard let r = modifier.modified(for: request) else {
333 completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
339 // There is a possiblility that request modifier changed the url to `nil` or empty.
340 guard let url = request.url, !url.absoluteString.isEmpty else {
341 completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.invalidURL.rawValue, userInfo: nil), nil, nil)
345 var downloadTask: RetrieveImageDownloadTask?
346 setup(progressBlock: progressBlock, with: completionHandler, for: url, options: options) {(session, fetchLoad) -> Void in
347 if fetchLoad.downloadTask == nil {
348 let dataTask = session.dataTask(with: request)
350 fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self)
352 dataTask.priority = options?.downloadPriority ?? URLSessionTask.defaultPriority
354 self.delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
356 // Hold self while the task is executing.
357 self.sessionHandler.downloadHolder = self
360 fetchLoad.downloadTaskCount += 1
361 downloadTask = fetchLoad.downloadTask
363 retrieveImageTask?.downloadTask = downloadTask
370 // MARK: - Download method
371 extension ImageDownloader {
373 // A single key may have multiple callbacks. Only download once.
374 func setup(progressBlock: ImageDownloaderProgressBlock?, with completionHandler: ImageDownloaderCompletionHandler?, for url: URL, options: KingfisherOptionsInfo?, started: @escaping ((URLSession, ImageFetchLoad) -> Void)) {
376 func prepareFetchLoad() {
377 barrierQueue.sync(flags: .barrier) {
378 let loadObjectForURL = fetchLoads[url] ?? ImageFetchLoad()
379 let callbackPair = (progressBlock: progressBlock, completionHandler: completionHandler)
381 loadObjectForURL.contents.append((callbackPair, options ?? KingfisherEmptyOptionsInfo))
383 fetchLoads[url] = loadObjectForURL
385 if let session = session {
386 started(session, loadObjectForURL)
391 if let fetchLoad = fetchLoad(for: url), fetchLoad.downloadTaskCount == 0 {
392 if fetchLoad.cancelSemaphore == nil {
393 fetchLoad.cancelSemaphore = DispatchSemaphore(value: 0)
396 _ = fetchLoad.cancelSemaphore?.wait(timeout: .distantFuture)
397 fetchLoad.cancelSemaphore = nil
405 private func cancelTaskImpl(_ task: RetrieveImageDownloadTask, fetchLoad: ImageFetchLoad? = nil, ignoreTaskCount: Bool = false) {
407 func getFetchLoad(from task: RetrieveImageDownloadTask) -> ImageFetchLoad? {
408 guard let URL = task.internalTask.originalRequest?.url,
409 let imageFetchLoad = self.fetchLoads[URL] else
413 return imageFetchLoad
416 guard let imageFetchLoad = fetchLoad ?? getFetchLoad(from: task) else {
420 imageFetchLoad.downloadTaskCount -= 1
421 if ignoreTaskCount || imageFetchLoad.downloadTaskCount == 0 {
422 task.internalTask.cancel()
426 func cancel(_ task: RetrieveImageDownloadTask) {
427 barrierQueue.sync(flags: .barrier) { cancelTaskImpl(task) }
430 /// Cancel all downloading tasks. It will trigger the completion handlers for all not-yet-finished
431 /// downloading tasks with an NSURLErrorCancelled error.
433 /// If you need to only cancel a certain task, call `cancel()` on the `RetrieveImageDownloadTask`
434 /// returned by the downloading methods.
435 public func cancelAll() {
436 barrierQueue.sync(flags: .barrier) {
437 fetchLoads.forEach { v in
438 let fetchLoad = v.value
439 guard let task = fetchLoad.downloadTask else { return }
440 cancelTaskImpl(task, fetchLoad: fetchLoad, ignoreTaskCount: true)
446 // MARK: - NSURLSessionDataDelegate
448 /// Delegate class for `NSURLSessionTaskDelegate`.
449 /// The session object will hold its delegate until it gets invalidated.
450 /// If we use `ImageDownloader` as the session delegate, it will not be released.
451 /// So we need an additional handler to break the retain cycle.
452 // See https://github.com/onevcat/Kingfisher/issues/235
453 final class ImageDownloaderSessionHandler: NSObject, URLSessionDataDelegate, AuthenticationChallengeResponsable {
455 // The holder will keep downloader not released while a data task is being executed.
456 // It will be set when the task started, and reset when the task finished.
457 var downloadHolder: ImageDownloader?
459 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
461 guard let downloader = downloadHolder else {
462 completionHandler(.cancel)
466 if let statusCode = (response as? HTTPURLResponse)?.statusCode,
467 let url = dataTask.originalRequest?.url,
468 !(downloader.delegate ?? downloader).isValidStatusCode(statusCode, for: downloader)
470 let error = NSError(domain: KingfisherErrorDomain,
471 code: KingfisherError.invalidStatusCode.rawValue,
472 userInfo: [KingfisherErrorStatusCodeKey: statusCode, NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: statusCode)])
473 callCompletionHandlerFailure(error: error, url: url)
476 completionHandler(.allow)
479 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
481 guard let downloader = downloadHolder else {
485 if let url = dataTask.originalRequest?.url, let fetchLoad = downloader.fetchLoad(for: url) {
486 fetchLoad.responseData.append(data)
488 if let expectedLength = dataTask.response?.expectedContentLength {
489 for content in fetchLoad.contents {
490 DispatchQueue.main.async {
491 content.callback.progressBlock?(Int64(fetchLoad.responseData.length), expectedLength)
498 func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
500 guard let url = task.originalRequest?.url else {
504 guard error == nil else {
505 callCompletionHandlerFailure(error: error!, url: url)
509 processImage(for: task, url: url)
513 This method is exposed since the compiler requests. Do not call it.
515 func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
516 guard let downloader = downloadHolder else {
520 downloader.authenticationChallengeResponder?.downloader(downloader, didReceive: challenge, completionHandler: completionHandler)
523 func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
524 guard let downloader = downloadHolder else {
528 downloader.authenticationChallengeResponder?.downloader(downloader, task: task, didReceive: challenge, completionHandler: completionHandler)
531 private func cleanFetchLoad(for url: URL) {
532 guard let downloader = downloadHolder else {
536 downloader.barrierQueue.sync(flags: .barrier) {
537 downloader.fetchLoads.removeValue(forKey: url)
538 if downloader.fetchLoads.isEmpty {
544 private func callCompletionHandlerFailure(error: Error, url: URL) {
545 guard let downloader = downloadHolder, let fetchLoad = downloader.fetchLoad(for: url) else {
549 // We need to clean the fetch load first, before actually calling completion handler.
550 cleanFetchLoad(for: url)
554 leftSignal = fetchLoad.cancelSemaphore?.signal() ?? 0
555 } while leftSignal != 0
557 for content in fetchLoad.contents {
558 content.options.callbackDispatchQueue.safeAsync {
559 content.callback.completionHandler?(nil, error as NSError, url, nil)
564 private func processImage(for task: URLSessionTask, url: URL) {
566 guard let downloader = downloadHolder else {
570 // We are on main queue when receiving this.
571 downloader.processQueue.async {
573 guard let fetchLoad = downloader.fetchLoad(for: url) else {
577 self.cleanFetchLoad(for: url)
580 let fetchedData = fetchLoad.responseData as Data
582 if let delegate = downloader.delegate {
583 data = delegate.imageDownloader(downloader, didDownload: fetchedData, for: url)
588 // Cache the processed images. So we do not need to re-process the image if using the same processor.
589 // Key is the identifier of processor.
590 var imageCache: [String: Image] = [:]
591 for content in fetchLoad.contents {
593 let options = content.options
594 let completionHandler = content.callback.completionHandler
595 let callbackQueue = options.callbackDispatchQueue
597 let processor = options.processor
598 var image = imageCache[processor.identifier]
599 if let data = data, image == nil {
600 image = processor.process(item: .data(data), options: options)
601 // Add the processed image to cache.
602 // If `image` is nil, nothing will happen (since the key is not existing before).
603 imageCache[processor.identifier] = image
606 if let image = image {
608 downloader.delegate?.imageDownloader(downloader, didDownload: image, for: url, with: task.response)
610 let imageModifier = options.imageModifier
611 let finalImage = imageModifier.modify(image)
613 if options.backgroundDecode {
614 let decodedImage = finalImage.kf.decoded
615 callbackQueue.safeAsync { completionHandler?(decodedImage, nil, url, data) }
617 callbackQueue.safeAsync { completionHandler?(finalImage, nil, url, data) }
621 if let res = task.response as? HTTPURLResponse , res.statusCode == 304 {
622 let notModified = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notModified.rawValue, userInfo: nil)
623 completionHandler?(nil, notModified, url, nil)
627 let badData = NSError(domain: KingfisherErrorDomain, code: KingfisherError.badData.rawValue, userInfo: nil)
628 callbackQueue.safeAsync { completionHandler?(nil, badData, url, nil) }
635 // Placeholder. For retrieving extension methods of ImageDownloaderDelegate
636 extension ImageDownloader: ImageDownloaderDelegate {}