Hibit_iOS/Pods/Kingfisher/Sources/General/KingfisherManager.swift
2024-06-07 11:41:02 +08:00

808 lines
35 KiB
Swift

//
// 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)")
}
}
}