KingfisherManager.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. //
  2. // KingfisherManager.swift
  3. // Kingfisher
  4. //
  5. // Created by Wei Wang on 15/4/6.
  6. //
  7. // Copyright (c) 2018 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. public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> Void)
  32. public typealias CompletionHandler = ((_ image: Image?, _ error: NSError?, _ cacheType: CacheType, _ imageURL: URL?) -> Void)
  33. /// RetrieveImageTask represents a task of image retrieving process.
  34. /// It contains an async task of getting image from disk and from network.
  35. public final class RetrieveImageTask {
  36. public static let empty = RetrieveImageTask()
  37. // If task is canceled before the download task started (which means the `downloadTask` is nil),
  38. // the download task should not begin.
  39. var cancelledBeforeDownloadStarting: Bool = false
  40. /// The network retrieve task in this image task.
  41. public var downloadTask: RetrieveImageDownloadTask?
  42. /**
  43. Cancel current task. If this task is already done, do nothing.
  44. */
  45. public func cancel() {
  46. if let downloadTask = downloadTask {
  47. downloadTask.cancel()
  48. } else {
  49. cancelledBeforeDownloadStarting = true
  50. }
  51. }
  52. }
  53. /// Error domain of Kingfisher
  54. public let KingfisherErrorDomain = "com.onevcat.Kingfisher.Error"
  55. /// Main manager class of Kingfisher. It connects Kingfisher downloader and cache.
  56. /// You can use this class to retrieve an image via a specified URL from web or cache.
  57. public class KingfisherManager {
  58. /// Shared manager used by the extensions across Kingfisher.
  59. public static let shared = KingfisherManager()
  60. /// Cache used by this manager
  61. public var cache: ImageCache
  62. /// Downloader used by this manager
  63. public var downloader: ImageDownloader
  64. /// Default options used by the manager. This option will be used in
  65. /// Kingfisher manager related methods, including all image view and
  66. /// button extension methods. You can also passing the options per image by
  67. /// sending an `options` parameter to Kingfisher's APIs, the per image option
  68. /// will overwrite the default ones if exist.
  69. ///
  70. /// - Note: This option will not be applied to independent using of `ImageDownloader` or `ImageCache`.
  71. public var defaultOptions = KingfisherEmptyOptionsInfo
  72. var currentDefaultOptions: KingfisherOptionsInfo {
  73. return [.downloader(downloader), .targetCache(cache)] + defaultOptions
  74. }
  75. fileprivate let processQueue: DispatchQueue
  76. convenience init() {
  77. self.init(downloader: .default, cache: .default)
  78. }
  79. init(downloader: ImageDownloader, cache: ImageCache) {
  80. self.downloader = downloader
  81. self.cache = cache
  82. let processQueueName = "com.onevcat.Kingfisher.KingfisherManager.processQueue.\(UUID().uuidString)"
  83. processQueue = DispatchQueue(label: processQueueName, attributes: .concurrent)
  84. }
  85. /**
  86. Get an image with resource.
  87. If KingfisherOptions.None is used as `options`, Kingfisher will seek the image in memory and disk first.
  88. If not found, it will download the image at `resource.downloadURL` and cache it with `resource.cacheKey`.
  89. These default behaviors could be adjusted by passing different options. See `KingfisherOptions` for more.
  90. - parameter resource: Resource object contains information such as `cacheKey` and `downloadURL`.
  91. - parameter options: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more.
  92. - parameter progressBlock: Called every time downloaded data changed. This could be used as a progress UI.
  93. - parameter completionHandler: Called when the whole retrieving process finished.
  94. - returns: A `RetrieveImageTask` task object. You can use this object to cancel the task.
  95. */
  96. @discardableResult
  97. public func retrieveImage(with resource: Resource,
  98. options: KingfisherOptionsInfo?,
  99. progressBlock: DownloadProgressBlock?,
  100. completionHandler: CompletionHandler?) -> RetrieveImageTask
  101. {
  102. let task = RetrieveImageTask()
  103. let options = currentDefaultOptions + (options ?? KingfisherEmptyOptionsInfo)
  104. if options.forceRefresh {
  105. _ = downloadAndCacheImage(
  106. with: resource.downloadURL,
  107. forKey: resource.cacheKey,
  108. retrieveImageTask: task,
  109. progressBlock: progressBlock,
  110. completionHandler: completionHandler,
  111. options: options)
  112. } else {
  113. tryToRetrieveImageFromCache(
  114. forKey: resource.cacheKey,
  115. with: resource.downloadURL,
  116. retrieveImageTask: task,
  117. progressBlock: progressBlock,
  118. completionHandler: completionHandler,
  119. options: options)
  120. }
  121. return task
  122. }
  123. @discardableResult
  124. func downloadAndCacheImage(with url: URL,
  125. forKey key: String,
  126. retrieveImageTask: RetrieveImageTask,
  127. progressBlock: DownloadProgressBlock?,
  128. completionHandler: CompletionHandler?,
  129. options: KingfisherOptionsInfo) -> RetrieveImageDownloadTask?
  130. {
  131. let downloader = options.downloader ?? self.downloader
  132. let processQueue = self.processQueue
  133. return downloader.downloadImage(with: url, retrieveImageTask: retrieveImageTask, options: options,
  134. progressBlock: { receivedSize, totalSize in
  135. progressBlock?(receivedSize, totalSize)
  136. },
  137. completionHandler: { image, error, imageURL, originalData in
  138. let targetCache = options.targetCache ?? self.cache
  139. if let error = error, error.code == KingfisherError.notModified.rawValue {
  140. // Not modified. Try to find the image from cache.
  141. // (The image should be in cache. It should be guaranteed by the framework users.)
  142. targetCache.retrieveImage(forKey: key, options: options, completionHandler: { (cacheImage, cacheType) -> Void in
  143. completionHandler?(cacheImage, nil, cacheType, url)
  144. })
  145. return
  146. }
  147. if let image = image, let originalData = originalData {
  148. targetCache.store(image,
  149. original: originalData,
  150. forKey: key,
  151. processorIdentifier:options.processor.identifier,
  152. cacheSerializer: options.cacheSerializer,
  153. toDisk: !options.cacheMemoryOnly,
  154. completionHandler: {
  155. guard options.waitForCache else { return }
  156. let cacheType = targetCache.imageCachedType(forKey: key, processorIdentifier: options.processor.identifier)
  157. completionHandler?(image, nil, cacheType, url)
  158. })
  159. if options.cacheOriginalImage && options.processor != DefaultImageProcessor.default {
  160. let originalCache = options.originalCache ?? targetCache
  161. let defaultProcessor = DefaultImageProcessor.default
  162. processQueue.async {
  163. if let originalImage = defaultProcessor.process(item: .data(originalData), options: options) {
  164. originalCache.store(originalImage,
  165. original: originalData,
  166. forKey: key,
  167. processorIdentifier: defaultProcessor.identifier,
  168. cacheSerializer: options.cacheSerializer,
  169. toDisk: !options.cacheMemoryOnly,
  170. completionHandler: nil)
  171. }
  172. }
  173. }
  174. }
  175. if options.waitForCache == false || image == nil {
  176. completionHandler?(image, error, .none, url)
  177. }
  178. })
  179. }
  180. func tryToRetrieveImageFromCache(forKey key: String,
  181. with url: URL,
  182. retrieveImageTask: RetrieveImageTask,
  183. progressBlock: DownloadProgressBlock?,
  184. completionHandler: CompletionHandler?,
  185. options: KingfisherOptionsInfo)
  186. {
  187. let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> Void in
  188. completionHandler?(image, error, cacheType, imageURL)
  189. }
  190. func handleNoCache() {
  191. if options.onlyFromCache {
  192. let error = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notCached.rawValue, userInfo: nil)
  193. diskTaskCompletionHandler(nil, error, .none, url)
  194. return
  195. }
  196. self.downloadAndCacheImage(
  197. with: url,
  198. forKey: key,
  199. retrieveImageTask: retrieveImageTask,
  200. progressBlock: progressBlock,
  201. completionHandler: diskTaskCompletionHandler,
  202. options: options)
  203. }
  204. let targetCache = options.targetCache ?? self.cache
  205. let processQueue = self.processQueue
  206. // First, try to get the exactly image from cache
  207. targetCache.retrieveImage(forKey: key, options: options) { image, cacheType in
  208. // If found, we could finish now.
  209. if image != nil {
  210. diskTaskCompletionHandler(image, nil, cacheType, url)
  211. return
  212. }
  213. // If not found, and we are using a default processor, download it!
  214. let processor = options.processor
  215. guard processor != DefaultImageProcessor.default else {
  216. handleNoCache()
  217. return
  218. }
  219. // If processor is not the default one, we have a chance to check whether
  220. // the original image is already in cache.
  221. let originalCache = options.originalCache ?? targetCache
  222. let optionsWithoutProcessor = options.removeAllMatchesIgnoringAssociatedValue(.processor(processor))
  223. originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { image, cacheType in
  224. // If we found the original image, there is no need to download it again.
  225. // We could just apply processor to it now.
  226. guard let image = image else {
  227. handleNoCache()
  228. return
  229. }
  230. processQueue.async {
  231. guard let processedImage = processor.process(item: .image(image), options: options) else {
  232. options.callbackDispatchQueue.safeAsync {
  233. diskTaskCompletionHandler(nil, nil, .none, url)
  234. }
  235. return
  236. }
  237. targetCache.store(processedImage,
  238. original: nil,
  239. forKey: key,
  240. processorIdentifier:options.processor.identifier,
  241. cacheSerializer: options.cacheSerializer,
  242. toDisk: !options.cacheMemoryOnly,
  243. completionHandler: {
  244. guard options.waitForCache else { return }
  245. let cacheType = targetCache.imageCachedType(forKey: key, processorIdentifier: options.processor.identifier)
  246. options.callbackDispatchQueue.safeAsync {
  247. diskTaskCompletionHandler(processedImage, nil, cacheType, url)
  248. }
  249. })
  250. if options.waitForCache == false {
  251. options.callbackDispatchQueue.safeAsync {
  252. diskTaskCompletionHandler(processedImage, nil, .none, url)
  253. }
  254. }
  255. }
  256. }
  257. }
  258. }
  259. }