2 // KingfisherManager.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 public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> Void)
34 public typealias CompletionHandler = ((_ image: Image?, _ error: NSError?, _ cacheType: CacheType, _ imageURL: URL?) -> Void)
36 /// RetrieveImageTask represents a task of image retrieving process.
37 /// It contains an async task of getting image from disk and from network.
38 public final class RetrieveImageTask {
40 public static let empty = RetrieveImageTask()
42 // If task is canceled before the download task started (which means the `downloadTask` is nil),
43 // the download task should not begin.
44 var cancelledBeforeDownloadStarting: Bool = false
46 /// The network retrieve task in this image task.
47 public var downloadTask: RetrieveImageDownloadTask?
50 Cancel current task. If this task is already done, do nothing.
52 public func cancel() {
53 if let downloadTask = downloadTask {
56 cancelledBeforeDownloadStarting = true
61 /// Error domain of Kingfisher
62 public let KingfisherErrorDomain = "com.onevcat.Kingfisher.Error"
64 /// Main manager class of Kingfisher. It connects Kingfisher downloader and cache.
65 /// You can use this class to retrieve an image via a specified URL from web or cache.
66 public class KingfisherManager {
68 /// Shared manager used by the extensions across Kingfisher.
69 public static let shared = KingfisherManager()
71 /// Cache used by this manager
72 public var cache: ImageCache
74 /// Downloader used by this manager
75 public var downloader: ImageDownloader
77 /// Default options used by the manager. This option will be used in
78 /// Kingfisher manager related methods, including all image view and
79 /// button extension methods. You can also passing the options per image by
80 /// sending an `options` parameter to Kingfisher's APIs, the per image option
81 /// will overwrite the default ones if exist.
83 /// - Note: This option will not be applied to independent using of `ImageDownloader` or `ImageCache`.
84 public var defaultOptions = KingfisherEmptyOptionsInfo
86 var currentDefaultOptions: KingfisherOptionsInfo {
87 return [.downloader(downloader), .targetCache(cache)] + defaultOptions
91 self.init(downloader: .default, cache: .default)
94 init(downloader: ImageDownloader, cache: ImageCache) {
95 self.downloader = downloader
100 Get an image with resource.
101 If KingfisherOptions.None is used as `options`, Kingfisher will seek the image in memory and disk first.
102 If not found, it will download the image at `resource.downloadURL` and cache it with `resource.cacheKey`.
103 These default behaviors could be adjusted by passing different options. See `KingfisherOptions` for more.
105 - parameter resource: Resource object contains information such as `cacheKey` and `downloadURL`.
106 - parameter options: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more.
107 - parameter progressBlock: Called every time downloaded data changed. This could be used as a progress UI.
108 - parameter completionHandler: Called when the whole retrieving process finished.
110 - returns: A `RetrieveImageTask` task object. You can use this object to cancel the task.
113 public func retrieveImage(with resource: Resource,
114 options: KingfisherOptionsInfo?,
115 progressBlock: DownloadProgressBlock?,
116 completionHandler: CompletionHandler?) -> RetrieveImageTask
118 let task = RetrieveImageTask()
119 let options = currentDefaultOptions + (options ?? KingfisherEmptyOptionsInfo)
120 if options.forceRefresh {
121 _ = downloadAndCacheImage(
122 with: resource.downloadURL,
123 forKey: resource.cacheKey,
124 retrieveImageTask: task,
125 progressBlock: progressBlock,
126 completionHandler: completionHandler,
129 tryToRetrieveImageFromCache(
130 forKey: resource.cacheKey,
131 with: resource.downloadURL,
132 retrieveImageTask: task,
133 progressBlock: progressBlock,
134 completionHandler: completionHandler,
142 func downloadAndCacheImage(with url: URL,
144 retrieveImageTask: RetrieveImageTask,
145 progressBlock: DownloadProgressBlock?,
146 completionHandler: CompletionHandler?,
147 options: KingfisherOptionsInfo) -> RetrieveImageDownloadTask?
149 let downloader = options.downloader
150 return downloader.downloadImage(with: url, retrieveImageTask: retrieveImageTask, options: options,
151 progressBlock: { receivedSize, totalSize in
152 progressBlock?(receivedSize, totalSize)
154 completionHandler: { image, error, imageURL, originalData in
156 let targetCache = options.targetCache
157 if let error = error, error.code == KingfisherError.notModified.rawValue {
158 // Not modified. Try to find the image from cache.
159 // (The image should be in cache. It should be guaranteed by the framework users.)
160 targetCache.retrieveImage(forKey: key, options: options, completionHandler: { (cacheImage, cacheType) -> Void in
161 completionHandler?(cacheImage, nil, cacheType, url)
166 if let image = image, let originalData = originalData {
167 targetCache.store(image,
168 original: originalData,
170 processorIdentifier:options.processor.identifier,
171 cacheSerializer: options.cacheSerializer,
172 toDisk: !options.cacheMemoryOnly,
173 completionHandler: nil)
174 if options.cacheOriginalImage && options.processor != DefaultImageProcessor.default {
175 let originalCache = options.originalCache
176 let defaultProcessor = DefaultImageProcessor.default
177 if let originalImage = defaultProcessor.process(item: .data(originalData), options: options) {
178 originalCache.store(originalImage,
179 original: originalData,
181 processorIdentifier: defaultProcessor.identifier,
182 cacheSerializer: options.cacheSerializer,
183 toDisk: !options.cacheMemoryOnly,
184 completionHandler: nil)
189 completionHandler?(image, error, .none, url)
194 func tryToRetrieveImageFromCache(forKey key: String,
196 retrieveImageTask: RetrieveImageTask,
197 progressBlock: DownloadProgressBlock?,
198 completionHandler: CompletionHandler?,
199 options: KingfisherOptionsInfo)
202 let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> Void in
203 completionHandler?(image, error, cacheType, imageURL)
206 func handleNoCache() {
207 if options.onlyFromCache {
208 let error = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notCached.rawValue, userInfo: nil)
209 diskTaskCompletionHandler(nil, error, .none, url)
212 self.downloadAndCacheImage(
215 retrieveImageTask: retrieveImageTask,
216 progressBlock: progressBlock,
217 completionHandler: diskTaskCompletionHandler,
222 let targetCache = options.targetCache
223 // First, try to get the exactly image from cache
224 targetCache.retrieveImage(forKey: key, options: options) { image, cacheType in
225 // If found, we could finish now.
227 diskTaskCompletionHandler(image, nil, cacheType, url)
231 // If not found, and we are using a default processor, download it!
232 let processor = options.processor
233 guard processor != DefaultImageProcessor.default else {
238 // If processor is not the default one, we have a chance to check whether
239 // the original image is already in cache.
240 let originalCache = options.originalCache
241 let optionsWithoutProcessor = options.removeAllMatchesIgnoringAssociatedValue(.processor(processor))
242 originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { image, cacheType in
243 // If we found the original image, there is no need to download it again.
244 // We could just apply processor to it now.
245 guard let image = image else {
250 guard let processedImage = processor.process(item: .image(image), options: options) else {
251 diskTaskCompletionHandler(nil, nil, .none, url)
254 targetCache.store(processedImage,
257 processorIdentifier:options.processor.identifier,
258 cacheSerializer: options.cacheSerializer,
259 toDisk: !options.cacheMemoryOnly,
260 completionHandler: nil)
261 diskTaskCompletionHandler(processedImage, nil, .none, url)