| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674 | ////  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?    {        let source = Source.network(resource)        return retrieveImage(            with: source,            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)")        }    }}
 |