// // KingfisherManager.swift // Kingfisher // // Created by Wei Wang on 15/4/6. // // Copyright (c) 2019 Wei Wang // // 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. import Foundation /// The downloading progress block type. /// The parameter value is the `receivedSize` of current response. /// The second parameter is the total expected data length from response's "Content-Length" header. /// If the expected length is not available, this block will not be called. public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> Void) /// Represents the result of a Kingfisher retrieving image task. public struct RetrieveImageResult { /// Gets the image object of this result. public let image: KFCrossPlatformImage /// Gets the cache source of the image. It indicates from which layer of cache this image is retrieved. /// If the image is just downloaded from network, `.none` will be returned. public let cacheType: CacheType /// The `Source` which this result is related to. This indicated where the `image` of `self` is referring. public let source: Source /// The original `Source` from which the retrieve task begins. It can be different from the `source` property. /// When an alternative source loading happened, the `source` will be the replacing loading target, while the /// `originalSource` will be kept as the initial `source` which issued the image loading process. public let originalSource: Source } /// A struct that stores some related information of an `KingfisherError`. It provides some context information for /// a pure error so you can identify the error easier. public struct PropagationError { /// The `Source` to which current `error` is bound. public let source: Source /// The actual error happens in framework. public let error: KingfisherError } /// The downloading task updated block type. The parameter `newTask` is the updated new task of image setting process. /// It is a `nil` if the image loading does not require an image downloading process. If an image downloading is issued, /// this value will contain the actual `DownloadTask` for you to keep and cancel it later if you need. public typealias DownloadTaskUpdatedBlock = ((_ newTask: DownloadTask?) -> Void) /// Main manager class of Kingfisher. It connects Kingfisher downloader and cache, /// to provide a set of convenience methods to use Kingfisher for tasks. /// You can use this class to retrieve an image via a specified URL from web or cache. public class KingfisherManager { /// Represents a shared manager used across Kingfisher. /// Use this instance for getting or storing images with Kingfisher. public static let shared = KingfisherManager() // Mark: Public Properties /// The `ImageCache` used by this manager. It is `ImageCache.default` by default. /// If a cache is specified in `KingfisherManager.defaultOptions`, the value in `defaultOptions` will be /// used instead. public var cache: ImageCache /// The `ImageDownloader` used by this manager. It is `ImageDownloader.default` by default. /// If a downloader is specified in `KingfisherManager.defaultOptions`, the value in `defaultOptions` will be /// used instead. public var downloader: ImageDownloader /// Default options used by the manager. This option will be used in /// Kingfisher manager related methods, as well as all view extension methods. /// You can also passing other options for each image task by sending an `options` parameter /// to Kingfisher's APIs. The per image options will overwrite the default ones, /// if the option exists in both. public var defaultOptions = KingfisherOptionsInfo.empty // Use `defaultOptions` to overwrite the `downloader` and `cache`. private var currentDefaultOptions: KingfisherOptionsInfo { return [.downloader(downloader), .targetCache(cache)] + defaultOptions } private let processingQueue: CallbackQueue private convenience init() { self.init(downloader: .default, cache: .default) } /// Creates an image setting manager with specified downloader and cache. /// /// - Parameters: /// - downloader: The image downloader used to download images. /// - cache: The image cache which stores memory and disk images. public init(downloader: ImageDownloader, cache: ImageCache) { self.downloader = downloader self.cache = cache let processQueueName = "com.onevcat.Kingfisher.KingfisherManager.processQueue.\(UUID().uuidString)" processingQueue = .dispatch(DispatchQueue(label: processQueueName)) } // MARK: - Getting Images /// Gets an image from a given resource. /// - Parameters: /// - resource: The `Resource` object defines data information like key or URL. /// - options: Options to use when creating the animated image. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. `progressBlock` is always called in /// main queue. /// - downloadTaskUpdated: Called when a new image downloading task is created for current image retrieving. This /// usually happens when an alternative source is used to replace the original (failed) /// task. You can update your reference of `DownloadTask` if you want to manually `cancel` /// the new task. /// - completionHandler: Called when the image retrieved and set finished. This completion handler will be invoked /// from the `options.callbackQueue`. If not specified, the main queue will be used. /// - Returns: A task represents the image downloading. If there is a download task starts for `.network` resource, /// the started `DownloadTask` is returned. Otherwise, `nil` is returned. /// /// - Note: /// This method will first check whether the requested `resource` is already in cache or not. If cached, /// it returns `nil` and invoke the `completionHandler` after the cached image retrieved. Otherwise, it /// will download the `resource`, store it in cache, then call `completionHandler`. @discardableResult public func retrieveImage( with resource: Resource, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, completionHandler: ((Result) -> Void)?) -> DownloadTask? { return retrieveImage( with: resource.convertToSource(), options: options, progressBlock: progressBlock, downloadTaskUpdated: downloadTaskUpdated, completionHandler: completionHandler ) } /// Gets an image from a given resource. /// /// - Parameters: /// - source: The `Source` object defines data information from network or a data provider. /// - options: Options to use when creating the animated image. /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an /// `expectedContentLength`, this block will not be called. `progressBlock` is always called in /// main queue. /// - downloadTaskUpdated: Called when a new image downloading task is created for current image retrieving. This /// usually happens when an alternative source is used to replace the original (failed) /// task. You can update your reference of `DownloadTask` if you want to manually `cancel` /// the new task. /// - completionHandler: Called when the image retrieved and set finished. This completion handler will be invoked /// from the `options.callbackQueue`. If not specified, the main queue will be used. /// - Returns: A task represents the image downloading. If there is a download task starts for `.network` resource, /// the started `DownloadTask` is returned. Otherwise, `nil` is returned. /// /// - Note: /// This method will first check whether the requested `source` is already in cache or not. If cached, /// it returns `nil` and invoke the `completionHandler` after the cached image retrieved. Otherwise, it /// will try to load the `source`, store it in cache, then call `completionHandler`. /// public func retrieveImage( with source: Source, options: KingfisherOptionsInfo? = nil, progressBlock: DownloadProgressBlock? = nil, downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, completionHandler: ((Result) -> Void)?) -> DownloadTask? { let options = currentDefaultOptions + (options ?? .empty) var info = KingfisherParsedOptionsInfo(options) if let block = progressBlock { info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] } return retrieveImage( with: source, options: info, downloadTaskUpdated: downloadTaskUpdated, completionHandler: completionHandler) } func retrieveImage( with source: Source, options: KingfisherParsedOptionsInfo, downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, completionHandler: ((Result) -> Void)?) -> DownloadTask? { var context = RetrievingContext(options: options, originalSource: source) func handler(currentSource: Source, result: (Result)) -> Void { switch result { case .success: completionHandler?(result) case .failure(let error): // Skip alternative sources if the user cancelled it. guard !error.isTaskCancelled else { completionHandler?(.failure(error)) return } if let nextSource = context.popAlternativeSource() { context.appendError(error, to: currentSource) let newTask = self.retrieveImage(with: nextSource, context: context) { result in handler(currentSource: nextSource, result: result) } downloadTaskUpdated?(newTask) } else { // No other alternative source. Finish with error. if context.propagationErrors.isEmpty { completionHandler?(.failure(error)) } else { context.appendError(error, to: currentSource) let finalError = KingfisherError.imageSettingError( reason: .alternativeSourcesExhausted(context.propagationErrors) ) completionHandler?(.failure(finalError)) } } } } return retrieveImage( with: source, context: context) { result in handler(currentSource: source, result: result) } } private func retrieveImage( with source: Source, context: RetrievingContext, completionHandler: ((Result) -> Void)?) -> DownloadTask? { let options = context.options if options.forceRefresh { return loadAndCacheImage( source: source, context: context, completionHandler: completionHandler)?.value } else { let loadedFromCache = retrieveImageFromCache( source: source, context: context, completionHandler: completionHandler) if loadedFromCache { return nil } if options.onlyFromCache { let error = KingfisherError.cacheError(reason: .imageNotExisting(key: source.cacheKey)) completionHandler?(.failure(error)) return nil } return loadAndCacheImage( source: source, context: context, completionHandler: completionHandler)?.value } } func provideImage( provider: ImageDataProvider, options: KingfisherParsedOptionsInfo, completionHandler: ((Result) -> Void)?) { guard let completionHandler = completionHandler else { return } provider.data { result in switch result { case .success(let data): (options.processingQueue ?? self.processingQueue).execute { let processor = options.processor let processingItem = ImageProcessItem.data(data) guard let image = processor.process(item: processingItem, options: options) else { options.callbackQueue.execute { let error = KingfisherError.processorError( reason: .processingFailed(processor: processor, item: processingItem)) completionHandler(.failure(error)) } return } options.callbackQueue.execute { let result = ImageLoadingResult(image: image, url: nil, originalData: data) completionHandler(.success(result)) } } case .failure(let error): options.callbackQueue.execute { let error = KingfisherError.imageSettingError( reason: .dataProviderError(provider: provider, error: error)) completionHandler(.failure(error)) } } } } private func cacheImage( source: Source, options: KingfisherParsedOptionsInfo, context: RetrievingContext, result: Result, completionHandler: ((Result) -> Void)? ) { switch result { case .success(let value): let needToCacheOriginalImage = options.cacheOriginalImage && options.processor != DefaultImageProcessor.default let coordinator = CacheCallbackCoordinator( shouldWaitForCache: options.waitForCache, shouldCacheOriginal: needToCacheOriginalImage) // Add image to cache. let targetCache = options.targetCache ?? self.cache targetCache.store( value.image, original: value.originalData, forKey: source.cacheKey, options: options, toDisk: !options.cacheMemoryOnly) { _ in coordinator.apply(.cachingImage) { let result = RetrieveImageResult( image: value.image, cacheType: .none, source: source, originalSource: context.originalSource ) completionHandler?(.success(result)) } } // Add original image to cache if necessary. if needToCacheOriginalImage { let originalCache = options.originalCache ?? targetCache originalCache.storeToDisk( value.originalData, forKey: source.cacheKey, processorIdentifier: DefaultImageProcessor.default.identifier, expiration: options.diskCacheExpiration) { _ in coordinator.apply(.cachingOriginalImage) { let result = RetrieveImageResult( image: value.image, cacheType: .none, source: source, originalSource: context.originalSource ) completionHandler?(.success(result)) } } } coordinator.apply(.cacheInitiated) { let result = RetrieveImageResult( image: value.image, cacheType: .none, source: source, originalSource: context.originalSource ) completionHandler?(.success(result)) } case .failure(let error): completionHandler?(.failure(error)) } } @discardableResult func loadAndCacheImage( source: Source, context: RetrievingContext, completionHandler: ((Result) -> Void)?) -> DownloadTask.WrappedTask? { let options = context.options func _cacheImage(_ result: Result) { cacheImage( source: source, options: options, context: context, result: result, completionHandler: completionHandler ) } switch source { case .network(let resource): let downloader = options.downloader ?? self.downloader let task = downloader.downloadImage( with: resource.downloadURL, options: options, completionHandler: _cacheImage ) return task.map(DownloadTask.WrappedTask.download) case .provider(let provider): provideImage(provider: provider, options: options, completionHandler: _cacheImage) return .dataProviding } } /// Retrieves image from memory or disk cache. /// /// - Parameters: /// - source: The target source from which to get image. /// - key: The key to use when caching the image. /// - url: Image request URL. This is not used when retrieving image from cache. It is just used for /// `RetrieveImageResult` callback compatibility. /// - options: Options on how to get the image from image cache. /// - completionHandler: Called when the image retrieving finishes, either with succeeded /// `RetrieveImageResult` or an error. /// - Returns: `true` if the requested image or the original image before being processed is existing in cache. /// Otherwise, this method returns `false`. /// /// - Note: /// The image retrieving could happen in either memory cache or disk cache. The `.processor` option in /// `options` will be considered when searching in the cache. If no processed image is found, Kingfisher /// will try to check whether an original version of that image is existing or not. If there is already an /// original, Kingfisher retrieves it from cache and processes it. Then, the processed image will be store /// back to cache for later use. func retrieveImageFromCache( source: Source, context: RetrievingContext, completionHandler: ((Result) -> Void)?) -> Bool { let options = context.options // 1. Check whether the image was already in target cache. If so, just get it. let targetCache = options.targetCache ?? cache let key = source.cacheKey let targetImageCached = targetCache.imageCachedType( forKey: key, processorIdentifier: options.processor.identifier) let validCache = targetImageCached.cached && (options.fromMemoryCacheOrRefresh == false || targetImageCached == .memory) if validCache { targetCache.retrieveImage(forKey: key, options: options) { result in guard let completionHandler = completionHandler else { return } options.callbackQueue.execute { result.match( onSuccess: { cacheResult in let value: Result if let image = cacheResult.image { value = result.map { RetrieveImageResult( image: image, cacheType: $0.cacheType, source: source, originalSource: context.originalSource ) } } else { value = .failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))) } completionHandler(value) }, onFailure: { _ in completionHandler(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key)))) } ) } } return true } // 2. Check whether the original image exists. If so, get it, process it, save to storage and return. let originalCache = options.originalCache ?? targetCache // No need to store the same file in the same cache again. if originalCache === targetCache && options.processor == DefaultImageProcessor.default { return false } // Check whether the unprocessed image existing or not. let originalImageCacheType = originalCache.imageCachedType( forKey: key, processorIdentifier: DefaultImageProcessor.default.identifier) let canAcceptDiskCache = !options.fromMemoryCacheOrRefresh let canUseOriginalImageCache = (canAcceptDiskCache && originalImageCacheType.cached) || (!canAcceptDiskCache && originalImageCacheType == .memory) if canUseOriginalImageCache { // Now we are ready to get found the original image from cache. We need the unprocessed image, so remove // any processor from options first. var optionsWithoutProcessor = options optionsWithoutProcessor.processor = DefaultImageProcessor.default originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { result in result.match( onSuccess: { cacheResult in guard let image = cacheResult.image else { assertionFailure("The image (under key: \(key) should be existing in the original cache.") return } let processor = options.processor (options.processingQueue ?? self.processingQueue).execute { let item = ImageProcessItem.image(image) guard let processedImage = processor.process(item: item, options: options) else { let error = KingfisherError.processorError( reason: .processingFailed(processor: processor, item: item)) options.callbackQueue.execute { completionHandler?(.failure(error)) } return } var cacheOptions = options cacheOptions.callbackQueue = .untouch let coordinator = CacheCallbackCoordinator( shouldWaitForCache: options.waitForCache, shouldCacheOriginal: false) targetCache.store( processedImage, forKey: key, options: cacheOptions, toDisk: !options.cacheMemoryOnly) { _ in coordinator.apply(.cachingImage) { let value = RetrieveImageResult( image: processedImage, cacheType: .none, source: source, originalSource: context.originalSource ) options.callbackQueue.execute { completionHandler?(.success(value)) } } } coordinator.apply(.cacheInitiated) { let value = RetrieveImageResult( image: processedImage, cacheType: .none, source: source, originalSource: context.originalSource ) options.callbackQueue.execute { completionHandler?(.success(value)) } } } }, onFailure: { _ in // This should not happen actually, since we already confirmed `originalImageCached` is `true`. // Just in case... options.callbackQueue.execute { completionHandler?( .failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))) ) } } ) } return true } return false } } struct RetrievingContext { var options: KingfisherParsedOptionsInfo let originalSource: Source var propagationErrors: [PropagationError] = [] init(options: KingfisherParsedOptionsInfo, originalSource: Source) { self.originalSource = originalSource self.options = options } mutating func popAlternativeSource() -> Source? { guard var alternativeSources = options.alternativeSources, !alternativeSources.isEmpty else { return nil } let nextSource = alternativeSources.removeFirst() options.alternativeSources = alternativeSources return nextSource } @discardableResult mutating func appendError(_ error: KingfisherError, to source: Source) -> [PropagationError] { let item = PropagationError(source: source, error: error) propagationErrors.append(item) return propagationErrors } } class CacheCallbackCoordinator { enum State { case idle case imageCached case originalImageCached case done } enum Action { case cacheInitiated case cachingImage case cachingOriginalImage } private let shouldWaitForCache: Bool private let shouldCacheOriginal: Bool private (set) var state: State = .idle init(shouldWaitForCache: Bool, shouldCacheOriginal: Bool) { self.shouldWaitForCache = shouldWaitForCache self.shouldCacheOriginal = shouldCacheOriginal } func apply(_ action: Action, trigger: () -> Void) { switch (state, action) { case (.done, _): break // From .idle case (.idle, .cacheInitiated): if !shouldWaitForCache { state = .done trigger() } case (.idle, .cachingImage): if shouldCacheOriginal { state = .imageCached } else { state = .done trigger() } case (.idle, .cachingOriginalImage): state = .originalImageCached // From .imageCached case (.imageCached, .cachingOriginalImage): state = .done trigger() // From .originalImageCached case (.originalImageCached, .cachingImage): state = .done trigger() default: assertionFailure("This case should not happen in CacheCallbackCoordinator: \(state) - \(action)") } } }