You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
673 lines
29 KiB
673 lines
29 KiB
//
|
|
// 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
|
|
|
|
/// 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<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 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<RetrieveImageResult, KingfisherError>) -> 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<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
|
|
{
|
|
var context = RetrievingContext(options: options, originalSource: source)
|
|
|
|
func handler(currentSource: Source, result: (Result<RetrieveImageResult, KingfisherError>)) -> 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<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)
|
|
// 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<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
|
|
)
|
|
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<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 }
|
|
options.callbackQueue.execute {
|
|
result.match(
|
|
onSuccess: { cacheResult in
|
|
let value: Result<RetrieveImageResult, KingfisherError>
|
|
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)")
|
|
}
|
|
}
|
|
}
|