123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807 |
- //
- // KingfisherManager.swift
- // Kingfisher
- //
- // Created by Wei Wang on 15/4/6.
- //
- // Copyright (c) 2019 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.
- import Foundation
- #if os(macOS)
- import AppKit
- #else
- import UIKit
- #endif
- /// 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
-
- /// Gets the data behind the result.
- ///
- /// If this result is from a network downloading (when `cacheType == .none`), calling this returns the downloaded
- /// data. If the reuslt is from cache, it serializes the image with the given cache serializer in the loading option
- /// and returns the result.
- ///
- /// - Note:
- /// This can be a time-consuming action, so if you need to use the data for multiple times, it is suggested to hold
- /// it and prevent keeping calling this too frequently.
- public let data: () -> Data?
- }
- /// 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 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<RetrieveImageResult, KingfisherError>) -> 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 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`.
- ///
- @discardableResult
- public func retrieveImage(
- with source: Source,
- options: KingfisherOptionsInfo? = nil,
- progressBlock: DownloadProgressBlock? = nil,
- downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil,
- completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
- {
- let options = currentDefaultOptions + (options ?? .empty)
- let info = KingfisherParsedOptionsInfo(options)
- return retrieveImage(
- with: source,
- options: info,
- progressBlock: progressBlock,
- downloadTaskUpdated: downloadTaskUpdated,
- completionHandler: completionHandler)
- }
- func retrieveImage(
- with source: Source,
- options: KingfisherParsedOptionsInfo,
- progressBlock: DownloadProgressBlock? = nil,
- downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil,
- completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
- {
- var info = options
- if let block = progressBlock {
- info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
- }
- return retrieveImage(
- with: source,
- options: info,
- downloadTaskUpdated: downloadTaskUpdated,
- progressiveImageSetter: nil,
- completionHandler: completionHandler)
- }
- func retrieveImage(
- with source: Source,
- options: KingfisherParsedOptionsInfo,
- downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil,
- progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil,
- referenceTaskIdentifierChecker: (() -> Bool)? = nil,
- completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
- {
- var options = options
- if let provider = ImageProgressiveProvider(options, refresh: { image in
- guard let setter = progressiveImageSetter else {
- return
- }
- guard let strategy = options.progressiveJPEG?.onImageUpdated(image) else {
- setter(image)
- return
- }
- switch strategy {
- case .default: setter(image)
- case .keepCurrent: break
- case .replace(let newImage): setter(newImage)
- }
- }) {
- options.onDataReceived = (options.onDataReceived ?? []) + [provider]
- }
- if let checker = referenceTaskIdentifierChecker {
- options.onDataReceived?.forEach {
- $0.onShouldApply = checker
- }
- }
-
- let retrievingContext = RetrievingContext(options: options, originalSource: source)
- var retryContext: RetryContext?
- func startNewRetrieveTask(
- with source: Source,
- downloadTaskUpdated: DownloadTaskUpdatedBlock?
- ) {
- let newTask = self.retrieveImage(with: source, context: retrievingContext) { result in
- handler(currentSource: source, result: result)
- }
- downloadTaskUpdated?(newTask)
- }
- func failCurrentSource(_ source: Source, with error: KingfisherError) {
- // Skip alternative sources if the user cancelled it.
- guard !error.isTaskCancelled else {
- completionHandler?(.failure(error))
- return
- }
- // When low data mode constrained error, retry with the low data mode source instead of use alternative on fly.
- guard !error.isLowDataModeConstrained else {
- if let source = retrievingContext.options.lowDataModeSource {
- retrievingContext.options.lowDataModeSource = nil
- startNewRetrieveTask(with: source, downloadTaskUpdated: downloadTaskUpdated)
- } else {
- // This should not happen.
- completionHandler?(.failure(error))
- }
- return
- }
- if let nextSource = retrievingContext.popAlternativeSource() {
- retrievingContext.appendError(error, to: source)
- startNewRetrieveTask(with: nextSource, downloadTaskUpdated: downloadTaskUpdated)
- } else {
- // No other alternative source. Finish with error.
- if retrievingContext.propagationErrors.isEmpty {
- completionHandler?(.failure(error))
- } else {
- retrievingContext.appendError(error, to: source)
- let finalError = KingfisherError.imageSettingError(
- reason: .alternativeSourcesExhausted(retrievingContext.propagationErrors)
- )
- completionHandler?(.failure(finalError))
- }
- }
- }
- func handler(currentSource: Source, result: (Result<RetrieveImageResult, KingfisherError>)) -> Void {
- switch result {
- case .success:
- completionHandler?(result)
- case .failure(let error):
- if let retryStrategy = options.retryStrategy {
- let context = retryContext?.increaseRetryCount() ?? RetryContext(source: source, error: error)
- retryContext = context
- retryStrategy.retry(context: context) { decision in
- switch decision {
- case .retry(let userInfo):
- retryContext?.userInfo = userInfo
- startNewRetrieveTask(with: source, downloadTaskUpdated: downloadTaskUpdated)
- case .stop:
- failCurrentSource(currentSource, with: error)
- }
- }
- } else {
- failCurrentSource(currentSource, with: error)
- }
- }
- }
- return retrieveImage(
- with: source,
- context: retrievingContext)
- {
- result in
- handler(currentSource: source, result: result)
- }
- }
-
- private func retrieveImage(
- with source: Source,
- context: RetrievingContext,
- completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> 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<ImageLoadingResult, KingfisherError>) -> 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<ImageLoadingResult, KingfisherError>,
- completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?
- )
- {
- switch result {
- case .success(let value):
- let needToCacheOriginalImage = options.cacheOriginalImage &&
- options.processor != DefaultImageProcessor.default
- let coordinator = CacheCallbackCoordinator(
- shouldWaitForCache: options.waitForCache, shouldCacheOriginal: needToCacheOriginalImage)
- let result = RetrieveImageResult(
- image: options.imageModifier?.modify(value.image) ?? value.image,
- cacheType: .none,
- source: source,
- originalSource: context.originalSource,
- data: { value.originalData }
- )
- // 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) {
- 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) {
- completionHandler?(.success(result))
- }
- }
- }
- coordinator.apply(.cacheInitiated) {
- completionHandler?(.success(result))
- }
- case .failure(let error):
- completionHandler?(.failure(error))
- }
- }
- @discardableResult
- func loadAndCacheImage(
- source: Source,
- context: RetrievingContext,
- completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask.WrappedTask?
- {
- let options = context.options
- func _cacheImage(_ result: Result<ImageLoadingResult, KingfisherError>) {
- 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
- )
- // The code below is neat, but it fails the Swift 5.2 compiler with a runtime crash when
- // `BUILD_LIBRARY_FOR_DISTRIBUTION` is turned on. I believe it is a bug in the compiler.
- // Let's fallback to a traditional style before it can be fixed in Swift.
- //
- // https://github.com/onevcat/Kingfisher/issues/1436
- //
- // return task.map(DownloadTask.WrappedTask.download)
- if let task = task {
- return .download(task)
- } else {
- return nil
- }
- 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<RetrieveImageResult, KingfisherError>) -> 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 }
-
- // TODO: Optimize it when we can use async across all the project.
- func checkResultImageAndCallback(_ inputImage: KFCrossPlatformImage) {
- var image = inputImage
- if image.kf.imageFrameCount != nil && image.kf.imageFrameCount != 1, let data = image.kf.animatedImageData {
- // Always recreate animated image representation since it is possible to be loaded in different options.
- // https://github.com/onevcat/Kingfisher/issues/1923
- image = options.processor.process(item: .data(data), options: options) ?? .init()
- }
- if let modifier = options.imageModifier {
- image = modifier.modify(image)
- }
- let value = result.map {
- RetrieveImageResult(
- image: image,
- cacheType: $0.cacheType,
- source: source,
- originalSource: context.originalSource,
- data: { options.cacheSerializer.data(with: image, original: nil) }
- )
- }
- completionHandler(value)
- }
-
- result.match { cacheResult in
- options.callbackQueue.execute {
- guard let image = cacheResult.image else {
- completionHandler(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))))
- return
- }
-
- if options.cacheSerializer.originalDataUsed {
- 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
- }
- options.callbackQueue.execute {
- checkResultImageAndCallback(processedImage)
- }
- }
- } else {
- checkResultImageAndCallback(image)
- }
- }
- } onFailure: { error in
- options.callbackQueue.execute {
- 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)
- let image = options.imageModifier?.modify(processedImage) ?? processedImage
- let result = RetrieveImageResult(
- image: image,
- cacheType: .none,
- source: source,
- originalSource: context.originalSource,
- data: { options.cacheSerializer.data(with: processedImage, original: nil) }
- )
- targetCache.store(
- processedImage,
- forKey: key,
- options: cacheOptions,
- toDisk: !options.cacheMemoryOnly)
- {
- _ in
- coordinator.apply(.cachingImage) {
- options.callbackQueue.execute { completionHandler?(.success(result)) }
- }
- }
- coordinator.apply(.cacheInitiated) {
- options.callbackQueue.execute { completionHandler?(.success(result)) }
- }
- }
- },
- 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
- }
- }
- class RetrievingContext {
- var options: KingfisherParsedOptionsInfo
- let originalSource: Source
- var propagationErrors: [PropagationError] = []
- init(options: KingfisherParsedOptionsInfo, originalSource: Source) {
- self.originalSource = originalSource
- self.options = options
- }
- func popAlternativeSource() -> Source? {
- guard var alternativeSources = options.alternativeSources, !alternativeSources.isEmpty else {
- return nil
- }
- let nextSource = alternativeSources.removeFirst()
- options.alternativeSources = alternativeSources
- return nextSource
- }
- @discardableResult
- 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 let stateQueue: DispatchQueue
- private var threadSafeState: State = .idle
- private (set) var state: State {
- set { stateQueue.sync { threadSafeState = newValue } }
- get { stateQueue.sync { threadSafeState } }
- }
- init(shouldWaitForCache: Bool, shouldCacheOriginal: Bool) {
- self.shouldWaitForCache = shouldWaitForCache
- self.shouldCacheOriginal = shouldCacheOriginal
- let stateQueueName = "com.onevcat.Kingfisher.CacheCallbackCoordinator.stateQueue.\(UUID().uuidString)"
- self.stateQueue = DispatchQueue(label: stateQueueName)
- }
- 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)")
- }
- }
- }
|