update
This commit is contained in:
132
Pods/Kingfisher/Sources/Cache/CacheSerializer.swift
generated
Normal file
132
Pods/Kingfisher/Sources/Cache/CacheSerializer.swift
generated
Normal file
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// CacheSerializer.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 2016/09/02.
|
||||
//
|
||||
// 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
|
||||
import CoreGraphics
|
||||
|
||||
/// An `CacheSerializer` is used to convert some data to an image object after
|
||||
/// retrieving it from disk storage, and vice versa, to convert an image to data object
|
||||
/// for storing to the disk storage.
|
||||
public protocol CacheSerializer {
|
||||
|
||||
/// Gets the serialized data from a provided image
|
||||
/// and optional original data for caching to disk.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - image: The image needed to be serialized.
|
||||
/// - original: The original data which is just downloaded.
|
||||
/// If the image is retrieved from cache instead of
|
||||
/// downloaded, it will be `nil`.
|
||||
/// - Returns: The data object for storing to disk, or `nil` when no valid
|
||||
/// data could be serialized.
|
||||
func data(with image: KFCrossPlatformImage, original: Data?) -> Data?
|
||||
|
||||
/// Gets an image from provided serialized data.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - data: The data from which an image should be deserialized.
|
||||
/// - options: The parsed options for deserialization.
|
||||
/// - Returns: An image deserialized or `nil` when no valid image
|
||||
/// could be deserialized.
|
||||
func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?
|
||||
|
||||
/// Whether this serializer prefers to cache the original data in its implementation.
|
||||
/// If `true`, after creating the image from the disk data, Kingfisher will continue to apply the processor to get
|
||||
/// the final image.
|
||||
///
|
||||
/// By default, it is `false` and the actual processed image is assumed to be serialized to the disk.
|
||||
var originalDataUsed: Bool { get }
|
||||
}
|
||||
|
||||
public extension CacheSerializer {
|
||||
var originalDataUsed: Bool { false }
|
||||
}
|
||||
|
||||
/// Represents a basic and default `CacheSerializer` used in Kingfisher disk cache system.
|
||||
/// It could serialize and deserialize images in PNG, JPEG and GIF format. For
|
||||
/// image other than these formats, a normalized `pngRepresentation` will be used.
|
||||
public struct DefaultCacheSerializer: CacheSerializer {
|
||||
|
||||
/// The default general cache serializer used across Kingfisher's cache.
|
||||
public static let `default` = DefaultCacheSerializer()
|
||||
|
||||
/// The compression quality when converting image to a lossy format data. Default is 1.0.
|
||||
public var compressionQuality: CGFloat = 1.0
|
||||
|
||||
/// Whether the original data should be preferred when serializing the image.
|
||||
/// If `true`, the input original data will be checked first and used unless the data is `nil`.
|
||||
/// In that case, the serialization will fall back to creating data from image.
|
||||
public var preferCacheOriginalData: Bool = false
|
||||
|
||||
/// Returnes the `preferCacheOriginalData` value. When the original data is used, Kingfisher needs to re-apply the
|
||||
/// processors to get the desired final image.
|
||||
public var originalDataUsed: Bool { preferCacheOriginalData }
|
||||
|
||||
/// Creates a cache serializer that serialize and deserialize images in PNG, JPEG and GIF format.
|
||||
///
|
||||
/// - Note:
|
||||
/// Use `DefaultCacheSerializer.default` unless you need to specify your own properties.
|
||||
///
|
||||
public init() { }
|
||||
|
||||
/// - Parameters:
|
||||
/// - image: The image needed to be serialized.
|
||||
/// - original: The original data which is just downloaded.
|
||||
/// If the image is retrieved from cache instead of
|
||||
/// downloaded, it will be `nil`.
|
||||
/// - Returns: The data object for storing to disk, or `nil` when no valid
|
||||
/// data could be serialized.
|
||||
///
|
||||
/// - Note:
|
||||
/// Only when `original` contains valid PNG, JPEG and GIF format data, the `image` will be
|
||||
/// converted to the corresponding data type. Otherwise, if the `original` is provided but it is not
|
||||
/// If `original` is `nil`, the input `image` will be encoded as PNG data.
|
||||
public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? {
|
||||
if preferCacheOriginalData {
|
||||
return original ??
|
||||
image.kf.data(
|
||||
format: original?.kf.imageFormat ?? .unknown,
|
||||
compressionQuality: compressionQuality
|
||||
)
|
||||
} else {
|
||||
return image.kf.data(
|
||||
format: original?.kf.imageFormat ?? .unknown,
|
||||
compressionQuality: compressionQuality
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets an image deserialized from provided data.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - data: The data from which an image should be deserialized.
|
||||
/// - options: Options for deserialization.
|
||||
/// - Returns: An image deserialized or `nil` when no valid image
|
||||
/// could be deserialized.
|
||||
public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions)
|
||||
}
|
||||
}
|
||||
588
Pods/Kingfisher/Sources/Cache/DiskStorage.swift
generated
Normal file
588
Pods/Kingfisher/Sources/Cache/DiskStorage.swift
generated
Normal file
@@ -0,0 +1,588 @@
|
||||
//
|
||||
// DiskStorage.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 2018/10/15.
|
||||
//
|
||||
// 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
|
||||
|
||||
|
||||
/// Represents a set of conception related to storage which stores a certain type of value in disk.
|
||||
/// This is a namespace for the disk storage types. A `Backend` with a certain `Config` will be used to describe the
|
||||
/// storage. See these composed types for more information.
|
||||
public enum DiskStorage {
|
||||
|
||||
/// Represents a storage back-end for the `DiskStorage`. The value is serialized to data
|
||||
/// and stored as file in the file system under a specified location.
|
||||
///
|
||||
/// You can config a `DiskStorage.Backend` in its initializer by passing a `DiskStorage.Config` value.
|
||||
/// or modifying the `config` property after it being created. `DiskStorage` will use file's attributes to keep
|
||||
/// track of a file for its expiration or size limitation.
|
||||
public class Backend<T: DataTransformable> {
|
||||
/// The config used for this disk storage.
|
||||
public var config: Config
|
||||
|
||||
// The final storage URL on disk, with `name` and `cachePathBlock` considered.
|
||||
public let directoryURL: URL
|
||||
|
||||
let metaChangingQueue: DispatchQueue
|
||||
|
||||
var maybeCached : Set<String>?
|
||||
let maybeCachedCheckingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.maybeCachedCheckingQueue")
|
||||
|
||||
// `false` if the storage initialized with an error. This prevents unexpected forcibly crash when creating
|
||||
// storage in the default cache.
|
||||
private var storageReady: Bool = true
|
||||
|
||||
/// Creates a disk storage with the given `DiskStorage.Config`.
|
||||
///
|
||||
/// - Parameter config: The config used for this disk storage.
|
||||
/// - Throws: An error if the folder for storage cannot be got or created.
|
||||
public convenience init(config: Config) throws {
|
||||
self.init(noThrowConfig: config, creatingDirectory: false)
|
||||
try prepareDirectory()
|
||||
}
|
||||
|
||||
// If `creatingDirectory` is `false`, the directory preparation will be skipped.
|
||||
// We need to call `prepareDirectory` manually after this returns.
|
||||
init(noThrowConfig config: Config, creatingDirectory: Bool) {
|
||||
var config = config
|
||||
|
||||
let creation = Creation(config)
|
||||
self.directoryURL = creation.directoryURL
|
||||
|
||||
// Break any possible retain cycle set by outside.
|
||||
config.cachePathBlock = nil
|
||||
self.config = config
|
||||
|
||||
metaChangingQueue = DispatchQueue(label: creation.cacheName)
|
||||
setupCacheChecking()
|
||||
|
||||
if creatingDirectory {
|
||||
try? prepareDirectory()
|
||||
}
|
||||
}
|
||||
|
||||
private func setupCacheChecking() {
|
||||
maybeCachedCheckingQueue.async {
|
||||
do {
|
||||
self.maybeCached = Set()
|
||||
try self.config.fileManager.contentsOfDirectory(atPath: self.directoryURL.path).forEach { fileName in
|
||||
self.maybeCached?.insert(fileName)
|
||||
}
|
||||
} catch {
|
||||
// Just disable the functionality if we fail to initialize it properly. This will just revert to
|
||||
// the behavior which is to check file existence on disk directly.
|
||||
self.maybeCached = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Creates the storage folder.
|
||||
private func prepareDirectory() throws {
|
||||
let fileManager = config.fileManager
|
||||
let path = directoryURL.path
|
||||
|
||||
guard !fileManager.fileExists(atPath: path) else { return }
|
||||
|
||||
do {
|
||||
try fileManager.createDirectory(
|
||||
atPath: path,
|
||||
withIntermediateDirectories: true,
|
||||
attributes: nil)
|
||||
} catch {
|
||||
self.storageReady = false
|
||||
throw KingfisherError.cacheError(reason: .cannotCreateDirectory(path: path, error: error))
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores a value to the storage under the specified key and expiration policy.
|
||||
/// - Parameters:
|
||||
/// - value: The value to be stored.
|
||||
/// - key: The key to which the `value` will be stored. If there is already a value under the key,
|
||||
/// the old value will be overwritten by `value`.
|
||||
/// - expiration: The expiration policy used by this store action.
|
||||
/// - writeOptions: Data writing options used the new files.
|
||||
/// - Throws: An error during converting the value to a data format or during writing it to disk.
|
||||
public func store(
|
||||
value: T,
|
||||
forKey key: String,
|
||||
expiration: StorageExpiration? = nil,
|
||||
writeOptions: Data.WritingOptions = []) throws
|
||||
{
|
||||
guard storageReady else {
|
||||
throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL))
|
||||
}
|
||||
|
||||
let expiration = expiration ?? config.expiration
|
||||
// The expiration indicates that already expired, no need to store.
|
||||
guard !expiration.isExpired else { return }
|
||||
|
||||
let data: Data
|
||||
do {
|
||||
data = try value.toData()
|
||||
} catch {
|
||||
throw KingfisherError.cacheError(reason: .cannotConvertToData(object: value, error: error))
|
||||
}
|
||||
|
||||
let fileURL = cacheFileURL(forKey: key)
|
||||
do {
|
||||
try data.write(to: fileURL, options: writeOptions)
|
||||
} catch {
|
||||
throw KingfisherError.cacheError(
|
||||
reason: .cannotCreateCacheFile(fileURL: fileURL, key: key, data: data, error: error)
|
||||
)
|
||||
}
|
||||
|
||||
let now = Date()
|
||||
let attributes: [FileAttributeKey : Any] = [
|
||||
// The last access date.
|
||||
.creationDate: now.fileAttributeDate,
|
||||
// The estimated expiration date.
|
||||
.modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate
|
||||
]
|
||||
do {
|
||||
try config.fileManager.setAttributes(attributes, ofItemAtPath: fileURL.path)
|
||||
} catch {
|
||||
try? config.fileManager.removeItem(at: fileURL)
|
||||
throw KingfisherError.cacheError(
|
||||
reason: .cannotSetCacheFileAttribute(
|
||||
filePath: fileURL.path,
|
||||
attributes: attributes,
|
||||
error: error
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
maybeCachedCheckingQueue.async {
|
||||
self.maybeCached?.insert(fileURL.lastPathComponent)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a value from the storage.
|
||||
/// - Parameters:
|
||||
/// - key: The cache key of value.
|
||||
/// - extendingExpiration: The expiration policy used by this getting action.
|
||||
/// - Throws: An error during converting the data to a value or during operation of disk files.
|
||||
/// - Returns: The value under `key` if it is valid and found in the storage. Otherwise, `nil`.
|
||||
public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) throws -> T? {
|
||||
return try value(forKey: key, referenceDate: Date(), actuallyLoad: true, extendingExpiration: extendingExpiration)
|
||||
}
|
||||
|
||||
func value(
|
||||
forKey key: String,
|
||||
referenceDate: Date,
|
||||
actuallyLoad: Bool,
|
||||
extendingExpiration: ExpirationExtending) throws -> T?
|
||||
{
|
||||
guard storageReady else {
|
||||
throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL))
|
||||
}
|
||||
|
||||
let fileManager = config.fileManager
|
||||
let fileURL = cacheFileURL(forKey: key)
|
||||
let filePath = fileURL.path
|
||||
|
||||
let fileMaybeCached = maybeCachedCheckingQueue.sync {
|
||||
return maybeCached?.contains(fileURL.lastPathComponent) ?? true
|
||||
}
|
||||
guard fileMaybeCached else {
|
||||
return nil
|
||||
}
|
||||
guard fileManager.fileExists(atPath: filePath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let meta: FileMeta
|
||||
do {
|
||||
let resourceKeys: Set<URLResourceKey> = [.contentModificationDateKey, .creationDateKey]
|
||||
meta = try FileMeta(fileURL: fileURL, resourceKeys: resourceKeys)
|
||||
} catch {
|
||||
throw KingfisherError.cacheError(
|
||||
reason: .invalidURLResource(error: error, key: key, url: fileURL))
|
||||
}
|
||||
|
||||
if meta.expired(referenceDate: referenceDate) {
|
||||
return nil
|
||||
}
|
||||
if !actuallyLoad { return T.empty }
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
let obj = try T.fromData(data)
|
||||
metaChangingQueue.async {
|
||||
meta.extendExpiration(with: fileManager, extendingExpiration: extendingExpiration)
|
||||
}
|
||||
return obj
|
||||
} catch {
|
||||
throw KingfisherError.cacheError(reason: .cannotLoadDataFromDisk(url: fileURL, error: error))
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether there is valid cached data under a given key.
|
||||
/// - Parameter key: The cache key of value.
|
||||
/// - Returns: If there is valid data under the key, `true`. Otherwise, `false`.
|
||||
///
|
||||
/// - Note:
|
||||
/// This method does not actually load the data from disk, so it is faster than directly loading the cached value
|
||||
/// by checking the nullability of `value(forKey:extendingExpiration:)` method.
|
||||
///
|
||||
public func isCached(forKey key: String) -> Bool {
|
||||
return isCached(forKey: key, referenceDate: Date())
|
||||
}
|
||||
|
||||
/// Whether there is valid cached data under a given key and a reference date.
|
||||
/// - Parameters:
|
||||
/// - key: The cache key of value.
|
||||
/// - referenceDate: A reference date to check whether the cache is still valid.
|
||||
/// - Returns: If there is valid data under the key, `true`. Otherwise, `false`.
|
||||
///
|
||||
/// - Note:
|
||||
/// If you pass `Date()` to `referenceDate`, this method is identical to `isCached(forKey:)`. Use the
|
||||
/// `referenceDate` to determine whether the cache is still valid for a future date.
|
||||
public func isCached(forKey key: String, referenceDate: Date) -> Bool {
|
||||
do {
|
||||
let result = try value(
|
||||
forKey: key,
|
||||
referenceDate: referenceDate,
|
||||
actuallyLoad: false,
|
||||
extendingExpiration: .none
|
||||
)
|
||||
return result != nil
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a value from a specified key.
|
||||
/// - Parameter key: The cache key of value.
|
||||
/// - Throws: An error during removing the value.
|
||||
public func remove(forKey key: String) throws {
|
||||
let fileURL = cacheFileURL(forKey: key)
|
||||
try removeFile(at: fileURL)
|
||||
}
|
||||
|
||||
func removeFile(at url: URL) throws {
|
||||
try config.fileManager.removeItem(at: url)
|
||||
}
|
||||
|
||||
/// Removes all values in this storage.
|
||||
/// - Throws: An error during removing the values.
|
||||
public func removeAll() throws {
|
||||
try removeAll(skipCreatingDirectory: false)
|
||||
}
|
||||
|
||||
func removeAll(skipCreatingDirectory: Bool) throws {
|
||||
try config.fileManager.removeItem(at: directoryURL)
|
||||
if !skipCreatingDirectory {
|
||||
try prepareDirectory()
|
||||
}
|
||||
}
|
||||
|
||||
/// The URL of the cached file with a given computed `key`.
|
||||
///
|
||||
/// - Parameter key: The final computed key used when caching the image. Please note that usually this is not
|
||||
/// the `cacheKey` of an image `Source`. It is the computed key with processor identifier considered.
|
||||
///
|
||||
/// - Note:
|
||||
/// This method does not guarantee there is an image already cached in the returned URL. It just gives your
|
||||
/// the URL that the image should be if it exists in disk storage, with the give key.
|
||||
///
|
||||
public func cacheFileURL(forKey key: String) -> URL {
|
||||
let fileName = cacheFileName(forKey: key)
|
||||
return directoryURL.appendingPathComponent(fileName, isDirectory: false)
|
||||
}
|
||||
|
||||
func cacheFileName(forKey key: String) -> String {
|
||||
if config.usesHashedFileName {
|
||||
let hashedKey = key.kf.md5
|
||||
if let ext = config.pathExtension {
|
||||
return "\(hashedKey).\(ext)"
|
||||
} else if config.autoExtAfterHashedFileName,
|
||||
let ext = key.kf.ext {
|
||||
return "\(hashedKey).\(ext)"
|
||||
}
|
||||
return hashedKey
|
||||
} else {
|
||||
if let ext = config.pathExtension {
|
||||
return "\(key).\(ext)"
|
||||
}
|
||||
return key
|
||||
}
|
||||
}
|
||||
|
||||
func allFileURLs(for propertyKeys: [URLResourceKey]) throws -> [URL] {
|
||||
let fileManager = config.fileManager
|
||||
|
||||
guard let directoryEnumerator = fileManager.enumerator(
|
||||
at: directoryURL, includingPropertiesForKeys: propertyKeys, options: .skipsHiddenFiles) else
|
||||
{
|
||||
throw KingfisherError.cacheError(reason: .fileEnumeratorCreationFailed(url: directoryURL))
|
||||
}
|
||||
|
||||
guard let urls = directoryEnumerator.allObjects as? [URL] else {
|
||||
throw KingfisherError.cacheError(reason: .invalidFileEnumeratorContent(url: directoryURL))
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
/// Removes all expired values from this storage.
|
||||
/// - Throws: A file manager error during removing the file.
|
||||
/// - Returns: The URLs for removed files.
|
||||
public func removeExpiredValues() throws -> [URL] {
|
||||
return try removeExpiredValues(referenceDate: Date())
|
||||
}
|
||||
|
||||
func removeExpiredValues(referenceDate: Date) throws -> [URL] {
|
||||
let propertyKeys: [URLResourceKey] = [
|
||||
.isDirectoryKey,
|
||||
.contentModificationDateKey
|
||||
]
|
||||
|
||||
let urls = try allFileURLs(for: propertyKeys)
|
||||
let keys = Set(propertyKeys)
|
||||
let expiredFiles = urls.filter { fileURL in
|
||||
do {
|
||||
let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys)
|
||||
if meta.isDirectory {
|
||||
return false
|
||||
}
|
||||
return meta.expired(referenceDate: referenceDate)
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
try expiredFiles.forEach { url in
|
||||
try removeFile(at: url)
|
||||
}
|
||||
return expiredFiles
|
||||
}
|
||||
|
||||
/// Removes all size exceeded values from this storage.
|
||||
/// - Throws: A file manager error during removing the file.
|
||||
/// - Returns: The URLs for removed files.
|
||||
///
|
||||
/// - Note: This method checks `config.sizeLimit` and remove cached files in an LRU (Least Recently Used) way.
|
||||
func removeSizeExceededValues() throws -> [URL] {
|
||||
|
||||
if config.sizeLimit == 0 { return [] } // Back compatible. 0 means no limit.
|
||||
|
||||
var size = try totalSize()
|
||||
if size < config.sizeLimit { return [] }
|
||||
|
||||
let propertyKeys: [URLResourceKey] = [
|
||||
.isDirectoryKey,
|
||||
.creationDateKey,
|
||||
.fileSizeKey
|
||||
]
|
||||
let keys = Set(propertyKeys)
|
||||
|
||||
let urls = try allFileURLs(for: propertyKeys)
|
||||
var pendings: [FileMeta] = urls.compactMap { fileURL in
|
||||
guard let meta = try? FileMeta(fileURL: fileURL, resourceKeys: keys) else {
|
||||
return nil
|
||||
}
|
||||
return meta
|
||||
}
|
||||
// Sort by last access date. Most recent file first.
|
||||
pendings.sort(by: FileMeta.lastAccessDate)
|
||||
|
||||
var removed: [URL] = []
|
||||
let target = config.sizeLimit / 2
|
||||
while size > target, let meta = pendings.popLast() {
|
||||
size -= UInt(meta.fileSize)
|
||||
try removeFile(at: meta.url)
|
||||
removed.append(meta.url)
|
||||
}
|
||||
return removed
|
||||
}
|
||||
|
||||
/// Gets the total file size of the folder in bytes.
|
||||
public func totalSize() throws -> UInt {
|
||||
let propertyKeys: [URLResourceKey] = [.fileSizeKey]
|
||||
let urls = try allFileURLs(for: propertyKeys)
|
||||
let keys = Set(propertyKeys)
|
||||
let totalSize: UInt = urls.reduce(0) { size, fileURL in
|
||||
do {
|
||||
let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys)
|
||||
return size + UInt(meta.fileSize)
|
||||
} catch {
|
||||
return size
|
||||
}
|
||||
}
|
||||
return totalSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DiskStorage {
|
||||
/// Represents the config used in a `DiskStorage`.
|
||||
public struct Config {
|
||||
|
||||
/// The file size limit on disk of the storage in bytes. 0 means no limit.
|
||||
public var sizeLimit: UInt
|
||||
|
||||
/// The `StorageExpiration` used in this disk storage. Default is `.days(7)`,
|
||||
/// means that the disk cache would expire in one week.
|
||||
public var expiration: StorageExpiration = .days(7)
|
||||
|
||||
/// The preferred extension of cache item. It will be appended to the file name as its extension.
|
||||
/// Default is `nil`, means that the cache file does not contain a file extension.
|
||||
public var pathExtension: String? = nil
|
||||
|
||||
/// Default is `true`, means that the cache file name will be hashed before storing.
|
||||
public var usesHashedFileName = true
|
||||
|
||||
/// Default is `false`
|
||||
/// If set to `true`, image extension will be extracted from original file name and append to
|
||||
/// the hased file name and used as the cache key on disk.
|
||||
public var autoExtAfterHashedFileName = false
|
||||
|
||||
/// Closure that takes in initial directory path and generates
|
||||
/// the final disk cache path. You can use it to fully customize your cache path.
|
||||
public var cachePathBlock: ((_ directory: URL, _ cacheName: String) -> URL)! = {
|
||||
(directory, cacheName) in
|
||||
return directory.appendingPathComponent(cacheName, isDirectory: true)
|
||||
}
|
||||
|
||||
let name: String
|
||||
let fileManager: FileManager
|
||||
let directory: URL?
|
||||
|
||||
/// Creates a config value based on given parameters.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The name of cache. It is used as a part of storage folder. It is used to identify the disk
|
||||
/// storage. Two storages with the same `name` would share the same folder in disk, and it should
|
||||
/// be prevented.
|
||||
/// - sizeLimit: The size limit in bytes for all existing files in the disk storage.
|
||||
/// - fileManager: The `FileManager` used to manipulate files on disk. Default is `FileManager.default`.
|
||||
/// - directory: The URL where the disk storage should live. The storage will use this as the root folder,
|
||||
/// and append a path which is constructed by input `name`. Default is `nil`, indicates that
|
||||
/// the cache directory under user domain mask will be used.
|
||||
public init(
|
||||
name: String,
|
||||
sizeLimit: UInt,
|
||||
fileManager: FileManager = .default,
|
||||
directory: URL? = nil)
|
||||
{
|
||||
self.name = name
|
||||
self.fileManager = fileManager
|
||||
self.directory = directory
|
||||
self.sizeLimit = sizeLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DiskStorage {
|
||||
struct FileMeta {
|
||||
|
||||
let url: URL
|
||||
|
||||
let lastAccessDate: Date?
|
||||
let estimatedExpirationDate: Date?
|
||||
let isDirectory: Bool
|
||||
let fileSize: Int
|
||||
|
||||
static func lastAccessDate(lhs: FileMeta, rhs: FileMeta) -> Bool {
|
||||
return lhs.lastAccessDate ?? .distantPast > rhs.lastAccessDate ?? .distantPast
|
||||
}
|
||||
|
||||
init(fileURL: URL, resourceKeys: Set<URLResourceKey>) throws {
|
||||
let meta = try fileURL.resourceValues(forKeys: resourceKeys)
|
||||
self.init(
|
||||
fileURL: fileURL,
|
||||
lastAccessDate: meta.creationDate,
|
||||
estimatedExpirationDate: meta.contentModificationDate,
|
||||
isDirectory: meta.isDirectory ?? false,
|
||||
fileSize: meta.fileSize ?? 0)
|
||||
}
|
||||
|
||||
init(
|
||||
fileURL: URL,
|
||||
lastAccessDate: Date?,
|
||||
estimatedExpirationDate: Date?,
|
||||
isDirectory: Bool,
|
||||
fileSize: Int)
|
||||
{
|
||||
self.url = fileURL
|
||||
self.lastAccessDate = lastAccessDate
|
||||
self.estimatedExpirationDate = estimatedExpirationDate
|
||||
self.isDirectory = isDirectory
|
||||
self.fileSize = fileSize
|
||||
}
|
||||
|
||||
func expired(referenceDate: Date) -> Bool {
|
||||
return estimatedExpirationDate?.isPast(referenceDate: referenceDate) ?? true
|
||||
}
|
||||
|
||||
func extendExpiration(with fileManager: FileManager, extendingExpiration: ExpirationExtending) {
|
||||
guard let lastAccessDate = lastAccessDate,
|
||||
let lastEstimatedExpiration = estimatedExpirationDate else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
let attributes: [FileAttributeKey : Any]
|
||||
|
||||
switch extendingExpiration {
|
||||
case .none:
|
||||
// not extending expiration time here
|
||||
return
|
||||
case .cacheTime:
|
||||
let originalExpiration: StorageExpiration =
|
||||
.seconds(lastEstimatedExpiration.timeIntervalSince(lastAccessDate))
|
||||
attributes = [
|
||||
.creationDate: Date().fileAttributeDate,
|
||||
.modificationDate: originalExpiration.estimatedExpirationSinceNow.fileAttributeDate
|
||||
]
|
||||
case .expirationTime(let expirationTime):
|
||||
attributes = [
|
||||
.creationDate: Date().fileAttributeDate,
|
||||
.modificationDate: expirationTime.estimatedExpirationSinceNow.fileAttributeDate
|
||||
]
|
||||
}
|
||||
|
||||
try? fileManager.setAttributes(attributes, ofItemAtPath: url.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DiskStorage {
|
||||
struct Creation {
|
||||
let directoryURL: URL
|
||||
let cacheName: String
|
||||
|
||||
init(_ config: Config) {
|
||||
let url: URL
|
||||
if let directory = config.directory {
|
||||
url = directory
|
||||
} else {
|
||||
url = config.fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
}
|
||||
|
||||
cacheName = "com.onevcat.Kingfisher.ImageCache.\(config.name)"
|
||||
directoryURL = config.cachePathBlock(url, cacheName)
|
||||
}
|
||||
}
|
||||
}
|
||||
118
Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift
generated
Normal file
118
Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift
generated
Normal file
@@ -0,0 +1,118 @@
|
||||
//
|
||||
// RequestModifier.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Junyu Kuang on 5/28/17.
|
||||
//
|
||||
// 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
|
||||
import CoreGraphics
|
||||
|
||||
/// `FormatIndicatedCacheSerializer` lets you indicate an image format for serialized caches.
|
||||
///
|
||||
/// It could serialize and deserialize PNG, JPEG and GIF images. For
|
||||
/// image other than these formats, a normalized `pngRepresentation` will be used.
|
||||
///
|
||||
/// Example:
|
||||
/// ````
|
||||
/// let profileImageSize = CGSize(width: 44, height: 44)
|
||||
///
|
||||
/// // A round corner image.
|
||||
/// let imageProcessor = RoundCornerImageProcessor(
|
||||
/// cornerRadius: profileImageSize.width / 2, targetSize: profileImageSize)
|
||||
///
|
||||
/// let optionsInfo: KingfisherOptionsInfo = [
|
||||
/// .cacheSerializer(FormatIndicatedCacheSerializer.png),
|
||||
/// .processor(imageProcessor)]
|
||||
///
|
||||
/// A URL pointing to a JPEG image.
|
||||
/// let url = URL(string: "https://example.com/image.jpg")!
|
||||
///
|
||||
/// // Image will be always cached as PNG format to preserve alpha channel for round rectangle.
|
||||
/// // So when you load it from cache again later, it will be still round cornered.
|
||||
/// // Otherwise, the corner part would be filled by white color (since JPEG does not contain an alpha channel).
|
||||
/// imageView.kf.setImage(with: url, options: optionsInfo)
|
||||
/// ````
|
||||
public struct FormatIndicatedCacheSerializer: CacheSerializer {
|
||||
|
||||
/// A `FormatIndicatedCacheSerializer` which converts image from and to PNG format. If the image cannot be
|
||||
/// represented by PNG format, it will fallback to its real format which is determined by `original` data.
|
||||
public static let png = FormatIndicatedCacheSerializer(imageFormat: .PNG, jpegCompressionQuality: nil)
|
||||
|
||||
/// A `FormatIndicatedCacheSerializer` which converts image from and to JPEG format. If the image cannot be
|
||||
/// represented by JPEG format, it will fallback to its real format which is determined by `original` data.
|
||||
/// The compression quality is 1.0 when using this serializer. If you need to set a customized compression quality,
|
||||
/// use `jpeg(compressionQuality:)`.
|
||||
public static let jpeg = FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: 1.0)
|
||||
|
||||
/// A `FormatIndicatedCacheSerializer` which converts image from and to JPEG format with a settable compression
|
||||
/// quality. If the image cannot be represented by JPEG format, it will fallback to its real format which is
|
||||
/// determined by `original` data.
|
||||
/// - Parameter compressionQuality: The compression quality when converting image to JPEG data.
|
||||
public static func jpeg(compressionQuality: CGFloat) -> FormatIndicatedCacheSerializer {
|
||||
return FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: compressionQuality)
|
||||
}
|
||||
|
||||
/// A `FormatIndicatedCacheSerializer` which converts image from and to GIF format. If the image cannot be
|
||||
/// represented by GIF format, it will fallback to its real format which is determined by `original` data.
|
||||
public static let gif = FormatIndicatedCacheSerializer(imageFormat: .GIF, jpegCompressionQuality: nil)
|
||||
|
||||
/// The indicated image format.
|
||||
private let imageFormat: ImageFormat
|
||||
|
||||
/// The compression quality used for loss image format (like JPEG).
|
||||
private let jpegCompressionQuality: CGFloat?
|
||||
|
||||
/// Creates data which represents the given `image` under a format.
|
||||
public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? {
|
||||
|
||||
func imageData(withFormat imageFormat: ImageFormat) -> Data? {
|
||||
return autoreleasepool { () -> Data? in
|
||||
switch imageFormat {
|
||||
case .PNG: return image.kf.pngRepresentation()
|
||||
case .JPEG: return image.kf.jpegRepresentation(compressionQuality: jpegCompressionQuality ?? 1.0)
|
||||
case .GIF: return image.kf.gifRepresentation()
|
||||
case .unknown: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generate data with indicated image format
|
||||
if let data = imageData(withFormat: imageFormat) {
|
||||
return data
|
||||
}
|
||||
|
||||
let originalFormat = original?.kf.imageFormat ?? .unknown
|
||||
|
||||
// generate data with original image's format
|
||||
if originalFormat != imageFormat, let data = imageData(withFormat: originalFormat) {
|
||||
return data
|
||||
}
|
||||
|
||||
return original ?? image.kf.normalized.kf.pngRepresentation()
|
||||
}
|
||||
|
||||
/// Same implementation as `DefaultCacheSerializer`.
|
||||
public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions)
|
||||
}
|
||||
}
|
||||
882
Pods/Kingfisher/Sources/Cache/ImageCache.swift
generated
Normal file
882
Pods/Kingfisher/Sources/Cache/ImageCache.swift
generated
Normal file
@@ -0,0 +1,882 @@
|
||||
//
|
||||
// ImageCache.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.
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
extension Notification.Name {
|
||||
/// This notification will be sent when the disk cache got cleaned either there are cached files expired or the
|
||||
/// total size exceeding the max allowed size. The manually invoking of `clearDiskCache` method will not trigger
|
||||
/// this notification.
|
||||
///
|
||||
/// The `object` of this notification is the `ImageCache` object which sends the notification.
|
||||
/// A list of removed hashes (files) could be retrieved by accessing the array under
|
||||
/// `KingfisherDiskCacheCleanedHashKey` key in `userInfo` of the notification object you received.
|
||||
/// By checking the array, you could know the hash codes of files are removed.
|
||||
public static let KingfisherDidCleanDiskCache =
|
||||
Notification.Name("com.onevcat.Kingfisher.KingfisherDidCleanDiskCache")
|
||||
}
|
||||
|
||||
/// Key for array of cleaned hashes in `userInfo` of `KingfisherDidCleanDiskCacheNotification`.
|
||||
public let KingfisherDiskCacheCleanedHashKey = "com.onevcat.Kingfisher.cleanedHash"
|
||||
|
||||
/// Cache type of a cached image.
|
||||
/// - none: The image is not cached yet when retrieving it.
|
||||
/// - memory: The image is cached in memory.
|
||||
/// - disk: The image is cached in disk.
|
||||
public enum CacheType {
|
||||
/// The image is not cached yet when retrieving it.
|
||||
case none
|
||||
/// The image is cached in memory.
|
||||
case memory
|
||||
/// The image is cached in disk.
|
||||
case disk
|
||||
|
||||
/// Whether the cache type represents the image is already cached or not.
|
||||
public var cached: Bool {
|
||||
switch self {
|
||||
case .memory, .disk: return true
|
||||
case .none: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the caching operation result.
|
||||
public struct CacheStoreResult {
|
||||
|
||||
/// The cache result for memory cache. Caching an image to memory will never fail.
|
||||
public let memoryCacheResult: Result<(), Never>
|
||||
|
||||
/// The cache result for disk cache. If an error happens during caching operation,
|
||||
/// you can get it from `.failure` case of this `diskCacheResult`.
|
||||
public let diskCacheResult: Result<(), KingfisherError>
|
||||
}
|
||||
|
||||
extension KFCrossPlatformImage: CacheCostCalculable {
|
||||
/// Cost of an image
|
||||
public var cacheCost: Int { return kf.cost }
|
||||
}
|
||||
|
||||
extension Data: DataTransformable {
|
||||
public func toData() throws -> Data {
|
||||
return self
|
||||
}
|
||||
|
||||
public static func fromData(_ data: Data) throws -> Data {
|
||||
return data
|
||||
}
|
||||
|
||||
public static let empty = Data()
|
||||
}
|
||||
|
||||
|
||||
/// Represents the getting image operation from the cache.
|
||||
///
|
||||
/// - disk: The image can be retrieved from disk cache.
|
||||
/// - memory: The image can be retrieved memory cache.
|
||||
/// - none: The image does not exist in the cache.
|
||||
public enum ImageCacheResult {
|
||||
|
||||
/// The image can be retrieved from disk cache.
|
||||
case disk(KFCrossPlatformImage)
|
||||
|
||||
/// The image can be retrieved memory cache.
|
||||
case memory(KFCrossPlatformImage)
|
||||
|
||||
/// The image does not exist in the cache.
|
||||
case none
|
||||
|
||||
/// Extracts the image from cache result. It returns the associated `Image` value for
|
||||
/// `.disk` and `.memory` case. For `.none` case, `nil` is returned.
|
||||
public var image: KFCrossPlatformImage? {
|
||||
switch self {
|
||||
case .disk(let image): return image
|
||||
case .memory(let image): return image
|
||||
case .none: return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the corresponding `CacheType` value based on the result type of `self`.
|
||||
public var cacheType: CacheType {
|
||||
switch self {
|
||||
case .disk: return .disk
|
||||
case .memory: return .memory
|
||||
case .none: return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a hybrid caching system which is composed by a `MemoryStorage.Backend` and a `DiskStorage.Backend`.
|
||||
/// `ImageCache` is a high level abstract for storing an image as well as its data to memory and disk, and
|
||||
/// retrieving them back.
|
||||
///
|
||||
/// While a default image cache object will be used if you prefer the extension methods of Kingfisher, you can create
|
||||
/// your own cache object and configure its storages as your need. This class also provide an interface for you to set
|
||||
/// the memory and disk storage config.
|
||||
open class ImageCache {
|
||||
|
||||
// MARK: Singleton
|
||||
/// The default `ImageCache` object. Kingfisher will use this cache for its related methods if there is no
|
||||
/// other cache specified. The `name` of this default cache is "default", and you should not use this name
|
||||
/// for any of your customize cache.
|
||||
public static let `default` = ImageCache(name: "default")
|
||||
|
||||
|
||||
// MARK: Public Properties
|
||||
/// The `MemoryStorage.Backend` object used in this cache. This storage holds loaded images in memory with a
|
||||
/// reasonable expire duration and a maximum memory usage. To modify the configuration of a storage, just set
|
||||
/// the storage `config` and its properties.
|
||||
public let memoryStorage: MemoryStorage.Backend<KFCrossPlatformImage>
|
||||
|
||||
/// The `DiskStorage.Backend` object used in this cache. This storage stores loaded images in disk with a
|
||||
/// reasonable expire duration and a maximum disk usage. To modify the configuration of a storage, just set
|
||||
/// the storage `config` and its properties.
|
||||
public let diskStorage: DiskStorage.Backend<Data>
|
||||
|
||||
private let ioQueue: DispatchQueue
|
||||
|
||||
/// Closure that defines the disk cache path from a given path and cacheName.
|
||||
public typealias DiskCachePathClosure = (URL, String) -> URL
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates an `ImageCache` from a customized `MemoryStorage` and `DiskStorage`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - memoryStorage: The `MemoryStorage.Backend` object to use in the image cache.
|
||||
/// - diskStorage: The `DiskStorage.Backend` object to use in the image cache.
|
||||
public init(
|
||||
memoryStorage: MemoryStorage.Backend<KFCrossPlatformImage>,
|
||||
diskStorage: DiskStorage.Backend<Data>)
|
||||
{
|
||||
self.memoryStorage = memoryStorage
|
||||
self.diskStorage = diskStorage
|
||||
let ioQueueName = "com.onevcat.Kingfisher.ImageCache.ioQueue.\(UUID().uuidString)"
|
||||
ioQueue = DispatchQueue(label: ioQueueName)
|
||||
|
||||
let notifications: [(Notification.Name, Selector)]
|
||||
#if !os(macOS) && !os(watchOS)
|
||||
notifications = [
|
||||
(UIApplication.didReceiveMemoryWarningNotification, #selector(clearMemoryCache)),
|
||||
(UIApplication.willTerminateNotification, #selector(cleanExpiredDiskCache)),
|
||||
(UIApplication.didEnterBackgroundNotification, #selector(backgroundCleanExpiredDiskCache))
|
||||
]
|
||||
#elseif os(macOS)
|
||||
notifications = [
|
||||
(NSApplication.willResignActiveNotification, #selector(cleanExpiredDiskCache)),
|
||||
]
|
||||
#else
|
||||
notifications = []
|
||||
#endif
|
||||
notifications.forEach {
|
||||
NotificationCenter.default.addObserver(self, selector: $0.1, name: $0.0, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an `ImageCache` with a given `name`. Both `MemoryStorage` and `DiskStorage` will be created
|
||||
/// with a default config based on the `name`.
|
||||
///
|
||||
/// - Parameter name: The name of cache object. It is used to setup disk cache directories and IO queue.
|
||||
/// You should not use the same `name` for different caches, otherwise, the disk storage would
|
||||
/// be conflicting to each other. The `name` should not be an empty string.
|
||||
public convenience init(name: String) {
|
||||
self.init(noThrowName: name, cacheDirectoryURL: nil, diskCachePathClosure: nil)
|
||||
}
|
||||
|
||||
/// Creates an `ImageCache` with a given `name`, cache directory `path`
|
||||
/// and a closure to modify the cache directory.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The name of cache object. It is used to setup disk cache directories and IO queue.
|
||||
/// You should not use the same `name` for different caches, otherwise, the disk storage would
|
||||
/// be conflicting to each other.
|
||||
/// - cacheDirectoryURL: Location of cache directory URL on disk. It will be internally pass to the
|
||||
/// initializer of `DiskStorage` as the disk cache directory. If `nil`, the cache
|
||||
/// directory under user domain mask will be used.
|
||||
/// - diskCachePathClosure: Closure that takes in an optional initial path string and generates
|
||||
/// the final disk cache path. You could use it to fully customize your cache path.
|
||||
/// - Throws: An error that happens during image cache creating, such as unable to create a directory at the given
|
||||
/// path.
|
||||
public convenience init(
|
||||
name: String,
|
||||
cacheDirectoryURL: URL?,
|
||||
diskCachePathClosure: DiskCachePathClosure? = nil
|
||||
) throws
|
||||
{
|
||||
if name.isEmpty {
|
||||
fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.")
|
||||
}
|
||||
|
||||
let memoryStorage = ImageCache.createMemoryStorage()
|
||||
|
||||
let config = ImageCache.createConfig(
|
||||
name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure
|
||||
)
|
||||
let diskStorage = try DiskStorage.Backend<Data>(config: config)
|
||||
self.init(memoryStorage: memoryStorage, diskStorage: diskStorage)
|
||||
}
|
||||
|
||||
convenience init(
|
||||
noThrowName name: String,
|
||||
cacheDirectoryURL: URL?,
|
||||
diskCachePathClosure: DiskCachePathClosure?
|
||||
)
|
||||
{
|
||||
if name.isEmpty {
|
||||
fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.")
|
||||
}
|
||||
|
||||
let memoryStorage = ImageCache.createMemoryStorage()
|
||||
|
||||
let config = ImageCache.createConfig(
|
||||
name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure
|
||||
)
|
||||
let diskStorage = DiskStorage.Backend<Data>(noThrowConfig: config, creatingDirectory: true)
|
||||
self.init(memoryStorage: memoryStorage, diskStorage: diskStorage)
|
||||
}
|
||||
|
||||
private static func createMemoryStorage() -> MemoryStorage.Backend<KFCrossPlatformImage> {
|
||||
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
||||
let costLimit = totalMemory / 4
|
||||
let memoryStorage = MemoryStorage.Backend<KFCrossPlatformImage>(config:
|
||||
.init(totalCostLimit: (costLimit > Int.max) ? Int.max : Int(costLimit)))
|
||||
return memoryStorage
|
||||
}
|
||||
|
||||
private static func createConfig(
|
||||
name: String,
|
||||
cacheDirectoryURL: URL?,
|
||||
diskCachePathClosure: DiskCachePathClosure? = nil
|
||||
) -> DiskStorage.Config
|
||||
{
|
||||
var diskConfig = DiskStorage.Config(
|
||||
name: name,
|
||||
sizeLimit: 0,
|
||||
directory: cacheDirectoryURL
|
||||
)
|
||||
if let closure = diskCachePathClosure {
|
||||
diskConfig.cachePathBlock = closure
|
||||
}
|
||||
return diskConfig
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: Storing Images
|
||||
|
||||
open func store(_ image: KFCrossPlatformImage,
|
||||
original: Data? = nil,
|
||||
forKey key: String,
|
||||
options: KingfisherParsedOptionsInfo,
|
||||
toDisk: Bool = true,
|
||||
completionHandler: ((CacheStoreResult) -> Void)? = nil)
|
||||
{
|
||||
let identifier = options.processor.identifier
|
||||
let callbackQueue = options.callbackQueue
|
||||
|
||||
let computedKey = key.computedKey(with: identifier)
|
||||
// Memory storage should not throw.
|
||||
memoryStorage.storeNoThrow(value: image, forKey: computedKey, expiration: options.memoryCacheExpiration)
|
||||
|
||||
guard toDisk else {
|
||||
if let completionHandler = completionHandler {
|
||||
let result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(()))
|
||||
callbackQueue.execute { completionHandler(result) }
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ioQueue.async {
|
||||
let serializer = options.cacheSerializer
|
||||
if let data = serializer.data(with: image, original: original) {
|
||||
self.syncStoreToDisk(
|
||||
data,
|
||||
forKey: key,
|
||||
processorIdentifier: identifier,
|
||||
callbackQueue: callbackQueue,
|
||||
expiration: options.diskCacheExpiration,
|
||||
writeOptions: options.diskStoreWriteOptions,
|
||||
completionHandler: completionHandler)
|
||||
} else {
|
||||
guard let completionHandler = completionHandler else { return }
|
||||
|
||||
let diskError = KingfisherError.cacheError(
|
||||
reason: .cannotSerializeImage(image: image, original: original, serializer: serializer))
|
||||
let result = CacheStoreResult(
|
||||
memoryCacheResult: .success(()),
|
||||
diskCacheResult: .failure(diskError))
|
||||
callbackQueue.execute { completionHandler(result) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores an image to the cache.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - image: The image to be stored.
|
||||
/// - original: The original data of the image. This value will be forwarded to the provided `serializer` for
|
||||
/// further use. By default, Kingfisher uses a `DefaultCacheSerializer` to serialize the image to
|
||||
/// data for caching in disk, it checks the image format based on `original` data to determine in
|
||||
/// which image format should be used. For other types of `serializer`, it depends on their
|
||||
/// implementation detail on how to use this original data.
|
||||
/// - key: The key used for caching the image.
|
||||
/// - identifier: The identifier of processor being used for caching. If you are using a processor for the
|
||||
/// image, pass the identifier of processor to this parameter.
|
||||
/// - serializer: The `CacheSerializer`
|
||||
/// - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory.
|
||||
/// Otherwise, it is cached in both memory storage and disk storage. Default is `true`.
|
||||
/// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`. For case
|
||||
/// that `toDisk` is `false`, a `.untouch` queue means `callbackQueue` will be invoked from the
|
||||
/// caller queue of this method. If `toDisk` is `true`, the `completionHandler` will be called
|
||||
/// from an internal file IO queue. To change this behavior, specify another `CallbackQueue`
|
||||
/// value.
|
||||
/// - completionHandler: A closure which is invoked when the cache operation finishes.
|
||||
open func store(_ image: KFCrossPlatformImage,
|
||||
original: Data? = nil,
|
||||
forKey key: String,
|
||||
processorIdentifier identifier: String = "",
|
||||
cacheSerializer serializer: CacheSerializer = DefaultCacheSerializer.default,
|
||||
toDisk: Bool = true,
|
||||
callbackQueue: CallbackQueue = .untouch,
|
||||
completionHandler: ((CacheStoreResult) -> Void)? = nil)
|
||||
{
|
||||
struct TempProcessor: ImageProcessor {
|
||||
let identifier: String
|
||||
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let options = KingfisherParsedOptionsInfo([
|
||||
.processor(TempProcessor(identifier: identifier)),
|
||||
.cacheSerializer(serializer),
|
||||
.callbackQueue(callbackQueue)
|
||||
])
|
||||
store(image, original: original, forKey: key, options: options,
|
||||
toDisk: toDisk, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
open func storeToDisk(
|
||||
_ data: Data,
|
||||
forKey key: String,
|
||||
processorIdentifier identifier: String = "",
|
||||
expiration: StorageExpiration? = nil,
|
||||
callbackQueue: CallbackQueue = .untouch,
|
||||
completionHandler: ((CacheStoreResult) -> Void)? = nil)
|
||||
{
|
||||
ioQueue.async {
|
||||
self.syncStoreToDisk(
|
||||
data,
|
||||
forKey: key,
|
||||
processorIdentifier: identifier,
|
||||
callbackQueue: callbackQueue,
|
||||
expiration: expiration,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
private func syncStoreToDisk(
|
||||
_ data: Data,
|
||||
forKey key: String,
|
||||
processorIdentifier identifier: String = "",
|
||||
callbackQueue: CallbackQueue = .untouch,
|
||||
expiration: StorageExpiration? = nil,
|
||||
writeOptions: Data.WritingOptions = [],
|
||||
completionHandler: ((CacheStoreResult) -> Void)? = nil)
|
||||
{
|
||||
let computedKey = key.computedKey(with: identifier)
|
||||
let result: CacheStoreResult
|
||||
do {
|
||||
try self.diskStorage.store(value: data, forKey: computedKey, expiration: expiration, writeOptions: writeOptions)
|
||||
result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(()))
|
||||
} catch {
|
||||
let diskError: KingfisherError
|
||||
if let error = error as? KingfisherError {
|
||||
diskError = error
|
||||
} else {
|
||||
diskError = .cacheError(reason: .cannotConvertToData(object: data, error: error))
|
||||
}
|
||||
|
||||
result = CacheStoreResult(
|
||||
memoryCacheResult: .success(()),
|
||||
diskCacheResult: .failure(diskError)
|
||||
)
|
||||
}
|
||||
if let completionHandler = completionHandler {
|
||||
callbackQueue.execute { completionHandler(result) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Removing Images
|
||||
|
||||
/// Removes the image for the given key from the cache.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - key: The key used for caching the image.
|
||||
/// - identifier: The identifier of processor being used for caching. If you are using a processor for the
|
||||
/// image, pass the identifier of processor to this parameter.
|
||||
/// - fromMemory: Whether this image should be removed from memory storage or not.
|
||||
/// If `false`, the image won't be removed from the memory storage. Default is `true`.
|
||||
/// - fromDisk: Whether this image should be removed from disk storage or not.
|
||||
/// If `false`, the image won't be removed from the disk storage. Default is `true`.
|
||||
/// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`.
|
||||
/// - completionHandler: A closure which is invoked when the cache removing operation finishes.
|
||||
open func removeImage(forKey key: String,
|
||||
processorIdentifier identifier: String = "",
|
||||
fromMemory: Bool = true,
|
||||
fromDisk: Bool = true,
|
||||
callbackQueue: CallbackQueue = .untouch,
|
||||
completionHandler: (() -> Void)? = nil)
|
||||
{
|
||||
let computedKey = key.computedKey(with: identifier)
|
||||
|
||||
if fromMemory {
|
||||
memoryStorage.remove(forKey: computedKey)
|
||||
}
|
||||
|
||||
if fromDisk {
|
||||
ioQueue.async{
|
||||
try? self.diskStorage.remove(forKey: computedKey)
|
||||
if let completionHandler = completionHandler {
|
||||
callbackQueue.execute { completionHandler() }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let completionHandler = completionHandler {
|
||||
callbackQueue.execute { completionHandler() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Getting Images
|
||||
|
||||
/// Gets an image for a given key from the cache, either from memory storage or disk storage.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - key: The key used for caching the image.
|
||||
/// - options: The `KingfisherParsedOptionsInfo` options setting used for retrieving the image.
|
||||
/// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.mainCurrentOrAsync`.
|
||||
/// - completionHandler: A closure which is invoked when the image getting operation finishes. If the
|
||||
/// image retrieving operation finishes without problem, an `ImageCacheResult` value
|
||||
/// will be sent to this closure as result. Otherwise, a `KingfisherError` result
|
||||
/// with detail failing reason will be sent.
|
||||
open func retrieveImage(
|
||||
forKey key: String,
|
||||
options: KingfisherParsedOptionsInfo,
|
||||
callbackQueue: CallbackQueue = .mainCurrentOrAsync,
|
||||
completionHandler: ((Result<ImageCacheResult, KingfisherError>) -> Void)?)
|
||||
{
|
||||
// No completion handler. No need to start working and early return.
|
||||
guard let completionHandler = completionHandler else { return }
|
||||
|
||||
// Try to check the image from memory cache first.
|
||||
if let image = retrieveImageInMemoryCache(forKey: key, options: options) {
|
||||
callbackQueue.execute { completionHandler(.success(.memory(image))) }
|
||||
} else if options.fromMemoryCacheOrRefresh {
|
||||
callbackQueue.execute { completionHandler(.success(.none)) }
|
||||
} else {
|
||||
|
||||
// Begin to disk search.
|
||||
self.retrieveImageInDiskCache(forKey: key, options: options, callbackQueue: callbackQueue) {
|
||||
result in
|
||||
switch result {
|
||||
case .success(let image):
|
||||
|
||||
guard let image = image else {
|
||||
// No image found in disk storage.
|
||||
callbackQueue.execute { completionHandler(.success(.none)) }
|
||||
return
|
||||
}
|
||||
|
||||
// Cache the disk image to memory.
|
||||
// We are passing `false` to `toDisk`, the memory cache does not change
|
||||
// callback queue, we can call `completionHandler` without another dispatch.
|
||||
var cacheOptions = options
|
||||
cacheOptions.callbackQueue = .untouch
|
||||
self.store(
|
||||
image,
|
||||
forKey: key,
|
||||
options: cacheOptions,
|
||||
toDisk: false)
|
||||
{
|
||||
_ in
|
||||
callbackQueue.execute { completionHandler(.success(.disk(image))) }
|
||||
}
|
||||
case .failure(let error):
|
||||
callbackQueue.execute { completionHandler(.failure(error)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets an image for a given key from the cache, either from memory storage or disk storage.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - key: The key used for caching the image.
|
||||
/// - options: The `KingfisherOptionsInfo` options setting used for retrieving the image.
|
||||
/// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.mainCurrentOrAsync`.
|
||||
/// - completionHandler: A closure which is invoked when the image getting operation finishes. If the
|
||||
/// image retrieving operation finishes without problem, an `ImageCacheResult` value
|
||||
/// will be sent to this closure as result. Otherwise, a `KingfisherError` result
|
||||
/// with detail failing reason will be sent.
|
||||
///
|
||||
/// Note: This method is marked as `open` for only compatible purpose. Do not overide this method. Instead, override
|
||||
/// the version receives `KingfisherParsedOptionsInfo` instead.
|
||||
open func retrieveImage(forKey key: String,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
callbackQueue: CallbackQueue = .mainCurrentOrAsync,
|
||||
completionHandler: ((Result<ImageCacheResult, KingfisherError>) -> Void)?)
|
||||
{
|
||||
retrieveImage(
|
||||
forKey: key,
|
||||
options: KingfisherParsedOptionsInfo(options),
|
||||
callbackQueue: callbackQueue,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/// Gets an image for a given key from the memory storage.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - key: The key used for caching the image.
|
||||
/// - options: The `KingfisherParsedOptionsInfo` options setting used for retrieving the image.
|
||||
/// - Returns: The image stored in memory cache, if exists and valid. Otherwise, if the image does not exist or
|
||||
/// has already expired, `nil` is returned.
|
||||
open func retrieveImageInMemoryCache(
|
||||
forKey key: String,
|
||||
options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?
|
||||
{
|
||||
let computedKey = key.computedKey(with: options.processor.identifier)
|
||||
return memoryStorage.value(forKey: computedKey, extendingExpiration: options.memoryCacheAccessExtendingExpiration)
|
||||
}
|
||||
|
||||
/// Gets an image for a given key from the memory storage.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - key: The key used for caching the image.
|
||||
/// - options: The `KingfisherOptionsInfo` options setting used for retrieving the image.
|
||||
/// - Returns: The image stored in memory cache, if exists and valid. Otherwise, if the image does not exist or
|
||||
/// has already expired, `nil` is returned.
|
||||
///
|
||||
/// Note: This method is marked as `open` for only compatible purpose. Do not overide this method. Instead, override
|
||||
/// the version receives `KingfisherParsedOptionsInfo` instead.
|
||||
open func retrieveImageInMemoryCache(
|
||||
forKey key: String,
|
||||
options: KingfisherOptionsInfo? = nil) -> KFCrossPlatformImage?
|
||||
{
|
||||
return retrieveImageInMemoryCache(forKey: key, options: KingfisherParsedOptionsInfo(options))
|
||||
}
|
||||
|
||||
func retrieveImageInDiskCache(
|
||||
forKey key: String,
|
||||
options: KingfisherParsedOptionsInfo,
|
||||
callbackQueue: CallbackQueue = .untouch,
|
||||
completionHandler: @escaping (Result<KFCrossPlatformImage?, KingfisherError>) -> Void)
|
||||
{
|
||||
let computedKey = key.computedKey(with: options.processor.identifier)
|
||||
let loadingQueue: CallbackQueue = options.loadDiskFileSynchronously ? .untouch : .dispatch(ioQueue)
|
||||
loadingQueue.execute {
|
||||
do {
|
||||
var image: KFCrossPlatformImage? = nil
|
||||
if let data = try self.diskStorage.value(forKey: computedKey, extendingExpiration: options.diskCacheAccessExtendingExpiration) {
|
||||
image = options.cacheSerializer.image(with: data, options: options)
|
||||
}
|
||||
if options.backgroundDecode {
|
||||
image = image?.kf.decoded(scale: options.scaleFactor)
|
||||
}
|
||||
callbackQueue.execute { completionHandler(.success(image)) }
|
||||
} catch let error as KingfisherError {
|
||||
callbackQueue.execute { completionHandler(.failure(error)) }
|
||||
} catch {
|
||||
assertionFailure("The internal thrown error should be a `KingfisherError`.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets an image for a given key from the disk storage.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - key: The key used for caching the image.
|
||||
/// - options: The `KingfisherOptionsInfo` options setting used for retrieving the image.
|
||||
/// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`.
|
||||
/// - completionHandler: A closure which is invoked when the operation finishes.
|
||||
open func retrieveImageInDiskCache(
|
||||
forKey key: String,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
callbackQueue: CallbackQueue = .untouch,
|
||||
completionHandler: @escaping (Result<KFCrossPlatformImage?, KingfisherError>) -> Void)
|
||||
{
|
||||
retrieveImageInDiskCache(
|
||||
forKey: key,
|
||||
options: KingfisherParsedOptionsInfo(options),
|
||||
callbackQueue: callbackQueue,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
// MARK: Cleaning
|
||||
/// Clears the memory & disk storage of this cache. This is an async operation.
|
||||
///
|
||||
/// - Parameter handler: A closure which is invoked when the cache clearing operation finishes.
|
||||
/// This `handler` will be called from the main queue.
|
||||
public func clearCache(completion handler: (() -> Void)? = nil) {
|
||||
clearMemoryCache()
|
||||
clearDiskCache(completion: handler)
|
||||
}
|
||||
|
||||
/// Clears the memory storage of this cache.
|
||||
@objc public func clearMemoryCache() {
|
||||
memoryStorage.removeAll()
|
||||
}
|
||||
|
||||
/// Clears the disk storage of this cache. This is an async operation.
|
||||
///
|
||||
/// - Parameter handler: A closure which is invoked when the cache clearing operation finishes.
|
||||
/// This `handler` will be called from the main queue.
|
||||
open func clearDiskCache(completion handler: (() -> Void)? = nil) {
|
||||
ioQueue.async {
|
||||
do {
|
||||
try self.diskStorage.removeAll()
|
||||
} catch _ { }
|
||||
if let handler = handler {
|
||||
DispatchQueue.main.async { handler() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the expired images from memory & disk storage. This is an async operation.
|
||||
open func cleanExpiredCache(completion handler: (() -> Void)? = nil) {
|
||||
cleanExpiredMemoryCache()
|
||||
cleanExpiredDiskCache(completion: handler)
|
||||
}
|
||||
|
||||
/// Clears the expired images from disk storage.
|
||||
open func cleanExpiredMemoryCache() {
|
||||
memoryStorage.removeExpired()
|
||||
}
|
||||
|
||||
/// Clears the expired images from disk storage. This is an async operation.
|
||||
@objc func cleanExpiredDiskCache() {
|
||||
cleanExpiredDiskCache(completion: nil)
|
||||
}
|
||||
|
||||
/// Clears the expired images from disk storage. This is an async operation.
|
||||
///
|
||||
/// - Parameter handler: A closure which is invoked when the cache clearing operation finishes.
|
||||
/// This `handler` will be called from the main queue.
|
||||
open func cleanExpiredDiskCache(completion handler: (() -> Void)? = nil) {
|
||||
ioQueue.async {
|
||||
do {
|
||||
var removed: [URL] = []
|
||||
let removedExpired = try self.diskStorage.removeExpiredValues()
|
||||
removed.append(contentsOf: removedExpired)
|
||||
|
||||
let removedSizeExceeded = try self.diskStorage.removeSizeExceededValues()
|
||||
removed.append(contentsOf: removedSizeExceeded)
|
||||
|
||||
if !removed.isEmpty {
|
||||
DispatchQueue.main.async {
|
||||
let cleanedHashes = removed.map { $0.lastPathComponent }
|
||||
NotificationCenter.default.post(
|
||||
name: .KingfisherDidCleanDiskCache,
|
||||
object: self,
|
||||
userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes])
|
||||
}
|
||||
}
|
||||
|
||||
if let handler = handler {
|
||||
DispatchQueue.main.async { handler() }
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(macOS) && !os(watchOS)
|
||||
/// Clears the expired images from disk storage when app is in background. This is an async operation.
|
||||
/// In most cases, you should not call this method explicitly.
|
||||
/// It will be called automatically when `UIApplicationDidEnterBackgroundNotification` received.
|
||||
@objc public func backgroundCleanExpiredDiskCache() {
|
||||
// if 'sharedApplication()' is unavailable, then return
|
||||
guard let sharedApplication = KingfisherWrapper<UIApplication>.shared else { return }
|
||||
|
||||
func endBackgroundTask(_ task: inout UIBackgroundTaskIdentifier) {
|
||||
sharedApplication.endBackgroundTask(task)
|
||||
task = UIBackgroundTaskIdentifier.invalid
|
||||
}
|
||||
|
||||
var backgroundTask: UIBackgroundTaskIdentifier!
|
||||
backgroundTask = sharedApplication.beginBackgroundTask {
|
||||
endBackgroundTask(&backgroundTask!)
|
||||
}
|
||||
|
||||
cleanExpiredDiskCache {
|
||||
endBackgroundTask(&backgroundTask!)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: Image Cache State
|
||||
|
||||
/// Returns the cache type for a given `key` and `identifier` combination.
|
||||
/// This method is used for checking whether an image is cached in current cache.
|
||||
/// It also provides information on which kind of cache can it be found in the return value.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - key: The key used for caching the image.
|
||||
/// - identifier: Processor identifier which used for this image. Default is the `identifier` of
|
||||
/// `DefaultImageProcessor.default`.
|
||||
/// - Returns: A `CacheType` instance which indicates the cache status.
|
||||
/// `.none` means the image is not in cache or it is already expired.
|
||||
open func imageCachedType(
|
||||
forKey key: String,
|
||||
processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> CacheType
|
||||
{
|
||||
let computedKey = key.computedKey(with: identifier)
|
||||
if memoryStorage.isCached(forKey: computedKey) { return .memory }
|
||||
if diskStorage.isCached(forKey: computedKey) { return .disk }
|
||||
return .none
|
||||
}
|
||||
|
||||
/// Returns whether the file exists in cache for a given `key` and `identifier` combination.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - key: The key used for caching the image.
|
||||
/// - identifier: Processor identifier which used for this image. Default is the `identifier` of
|
||||
/// `DefaultImageProcessor.default`.
|
||||
/// - Returns: A `Bool` which indicates whether a cache could match the given `key` and `identifier` combination.
|
||||
///
|
||||
/// - Note:
|
||||
/// The return value does not contain information about from which kind of storage the cache matches.
|
||||
/// To get the information about cache type according `CacheType`,
|
||||
/// use `imageCachedType(forKey:processorIdentifier:)` instead.
|
||||
public func isCached(
|
||||
forKey key: String,
|
||||
processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> Bool
|
||||
{
|
||||
return imageCachedType(forKey: key, processorIdentifier: identifier).cached
|
||||
}
|
||||
|
||||
/// Gets the hash used as cache file name for the key.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - key: The key used for caching the image.
|
||||
/// - identifier: Processor identifier which used for this image. Default is the `identifier` of
|
||||
/// `DefaultImageProcessor.default`.
|
||||
/// - Returns: The hash which is used as the cache file name.
|
||||
///
|
||||
/// - Note:
|
||||
/// By default, for a given combination of `key` and `identifier`, `ImageCache` will use the value
|
||||
/// returned by this method as the cache file name. You can use this value to check and match cache file
|
||||
/// if you need.
|
||||
open func hash(
|
||||
forKey key: String,
|
||||
processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> String
|
||||
{
|
||||
let computedKey = key.computedKey(with: identifier)
|
||||
return diskStorage.cacheFileName(forKey: computedKey)
|
||||
}
|
||||
|
||||
/// Calculates the size taken by the disk storage.
|
||||
/// It is the total file size of all cached files in the `diskStorage` on disk in bytes.
|
||||
///
|
||||
/// - Parameter handler: Called with the size calculating finishes. This closure is invoked from the main queue.
|
||||
open func calculateDiskStorageSize(completion handler: @escaping ((Result<UInt, KingfisherError>) -> Void)) {
|
||||
ioQueue.async {
|
||||
do {
|
||||
let size = try self.diskStorage.totalSize()
|
||||
DispatchQueue.main.async { handler(.success(size)) }
|
||||
} catch let error as KingfisherError {
|
||||
DispatchQueue.main.async { handler(.failure(error)) }
|
||||
} catch {
|
||||
assertionFailure("The internal thrown error should be a `KingfisherError`.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=5.5)
|
||||
#if canImport(_Concurrency)
|
||||
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
|
||||
open var diskStorageSize: UInt {
|
||||
get async throws {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
calculateDiskStorageSize { result in
|
||||
continuation.resume(with: result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/// Gets the cache path for the key.
|
||||
/// It is useful for projects with web view or anyone that needs access to the local file path.
|
||||
///
|
||||
/// i.e. Replacing the `<img src='path_for_key'>` tag in your HTML.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - key: The key used for caching the image.
|
||||
/// - identifier: Processor identifier which used for this image. Default is the `identifier` of
|
||||
/// `DefaultImageProcessor.default`.
|
||||
/// - Returns: The disk path of cached image under the given `key` and `identifier`.
|
||||
///
|
||||
/// - Note:
|
||||
/// This method does not guarantee there is an image already cached in the returned path. It just gives your
|
||||
/// the path that the image should be, if it exists in disk storage.
|
||||
///
|
||||
/// You could use `isCached(forKey:)` method to check whether the image is cached under that key in disk.
|
||||
open func cachePath(
|
||||
forKey key: String,
|
||||
processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> String
|
||||
{
|
||||
let computedKey = key.computedKey(with: identifier)
|
||||
return diskStorage.cacheFileURL(forKey: computedKey).path
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(macOS) && !os(watchOS)
|
||||
// MARK: - For App Extensions
|
||||
extension UIApplication: KingfisherCompatible { }
|
||||
extension KingfisherWrapper where Base: UIApplication {
|
||||
public static var shared: UIApplication? {
|
||||
let selector = NSSelectorFromString("sharedApplication")
|
||||
guard Base.responds(to: selector) else { return nil }
|
||||
return Base.perform(selector).takeUnretainedValue() as? UIApplication
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
extension String {
|
||||
func computedKey(with identifier: String) -> String {
|
||||
if identifier.isEmpty {
|
||||
return self
|
||||
} else {
|
||||
return appending("@\(identifier)")
|
||||
}
|
||||
}
|
||||
}
|
||||
283
Pods/Kingfisher/Sources/Cache/MemoryStorage.swift
generated
Normal file
283
Pods/Kingfisher/Sources/Cache/MemoryStorage.swift
generated
Normal file
@@ -0,0 +1,283 @@
|
||||
//
|
||||
// MemoryStorage.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 2018/10/15.
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Represents a set of conception related to storage which stores a certain type of value in memory.
|
||||
/// This is a namespace for the memory storage types. A `Backend` with a certain `Config` will be used to describe the
|
||||
/// storage. See these composed types for more information.
|
||||
public enum MemoryStorage {
|
||||
|
||||
/// Represents a storage which stores a certain type of value in memory. It provides fast access,
|
||||
/// but limited storing size. The stored value type needs to conform to `CacheCostCalculable`,
|
||||
/// and its `cacheCost` will be used to determine the cost of size for the cache item.
|
||||
///
|
||||
/// You can config a `MemoryStorage.Backend` in its initializer by passing a `MemoryStorage.Config` value.
|
||||
/// or modifying the `config` property after it being created. The backend of `MemoryStorage` has
|
||||
/// upper limitation on cost size in memory and item count. All items in the storage has an expiration
|
||||
/// date. When retrieved, if the target item is already expired, it will be recognized as it does not
|
||||
/// exist in the storage. The `MemoryStorage` also contains a scheduled self clean task, to evict expired
|
||||
/// items from memory.
|
||||
public class Backend<T: CacheCostCalculable> {
|
||||
let storage = NSCache<NSString, StorageObject<T>>()
|
||||
|
||||
// Keys trackes the objects once inside the storage. For object removing triggered by user, the corresponding
|
||||
// key would be also removed. However, for the object removing triggered by cache rule/policy of system, the
|
||||
// key will be remained there until next `removeExpired` happens.
|
||||
//
|
||||
// Breaking the strict tracking could save additional locking behaviors.
|
||||
// See https://github.com/onevcat/Kingfisher/issues/1233
|
||||
var keys = Set<String>()
|
||||
|
||||
private var cleanTimer: Timer? = nil
|
||||
private let lock = NSLock()
|
||||
|
||||
/// The config used in this storage. It is a value you can set and
|
||||
/// use to config the storage in air.
|
||||
public var config: Config {
|
||||
didSet {
|
||||
storage.totalCostLimit = config.totalCostLimit
|
||||
storage.countLimit = config.countLimit
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a `MemoryStorage` with a given `config`.
|
||||
///
|
||||
/// - Parameter config: The config used to create the storage. It determines the max size limitation,
|
||||
/// default expiration setting and more.
|
||||
public init(config: Config) {
|
||||
self.config = config
|
||||
storage.totalCostLimit = config.totalCostLimit
|
||||
storage.countLimit = config.countLimit
|
||||
|
||||
cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
self.removeExpired()
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the expired values from the storage.
|
||||
public func removeExpired() {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
for key in keys {
|
||||
let nsKey = key as NSString
|
||||
guard let object = storage.object(forKey: nsKey) else {
|
||||
// This could happen if the object is moved by cache `totalCostLimit` or `countLimit` rule.
|
||||
// We didn't remove the key yet until now, since we do not want to introduce additional lock.
|
||||
// See https://github.com/onevcat/Kingfisher/issues/1233
|
||||
keys.remove(key)
|
||||
continue
|
||||
}
|
||||
if object.isExpired {
|
||||
storage.removeObject(forKey: nsKey)
|
||||
keys.remove(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores a value to the storage under the specified key and expiration policy.
|
||||
/// - Parameters:
|
||||
/// - value: The value to be stored.
|
||||
/// - key: The key to which the `value` will be stored.
|
||||
/// - expiration: The expiration policy used by this store action.
|
||||
/// - Throws: No error will
|
||||
public func store(
|
||||
value: T,
|
||||
forKey key: String,
|
||||
expiration: StorageExpiration? = nil)
|
||||
{
|
||||
storeNoThrow(value: value, forKey: key, expiration: expiration)
|
||||
}
|
||||
|
||||
// The no throw version for storing value in cache. Kingfisher knows the detail so it
|
||||
// could use this version to make syntax simpler internally.
|
||||
func storeNoThrow(
|
||||
value: T,
|
||||
forKey key: String,
|
||||
expiration: StorageExpiration? = nil)
|
||||
{
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let expiration = expiration ?? config.expiration
|
||||
// The expiration indicates that already expired, no need to store.
|
||||
guard !expiration.isExpired else { return }
|
||||
|
||||
let object: StorageObject<T>
|
||||
if config.keepWhenEnteringBackground {
|
||||
object = BackgroundKeepingStorageObject(value, expiration: expiration)
|
||||
} else {
|
||||
object = StorageObject(value, expiration: expiration)
|
||||
}
|
||||
storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)
|
||||
keys.insert(key)
|
||||
}
|
||||
|
||||
/// Gets a value from the storage.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - key: The cache key of value.
|
||||
/// - extendingExpiration: The expiration policy used by this getting action.
|
||||
/// - Returns: The value under `key` if it is valid and found in the storage. Otherwise, `nil`.
|
||||
public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? {
|
||||
guard let object = storage.object(forKey: key as NSString) else {
|
||||
return nil
|
||||
}
|
||||
if object.isExpired {
|
||||
return nil
|
||||
}
|
||||
object.extendExpiration(extendingExpiration)
|
||||
return object.value
|
||||
}
|
||||
|
||||
/// Whether there is valid cached data under a given key.
|
||||
/// - Parameter key: The cache key of value.
|
||||
/// - Returns: If there is valid data under the key, `true`. Otherwise, `false`.
|
||||
public func isCached(forKey key: String) -> Bool {
|
||||
guard let _ = value(forKey: key, extendingExpiration: .none) else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/// Removes a value from a specified key.
|
||||
/// - Parameter key: The cache key of value.
|
||||
public func remove(forKey key: String) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
storage.removeObject(forKey: key as NSString)
|
||||
keys.remove(key)
|
||||
}
|
||||
|
||||
/// Removes all values in this storage.
|
||||
public func removeAll() {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
storage.removeAllObjects()
|
||||
keys.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MemoryStorage {
|
||||
/// Represents the config used in a `MemoryStorage`.
|
||||
public struct Config {
|
||||
|
||||
/// Total cost limit of the storage in bytes.
|
||||
public var totalCostLimit: Int
|
||||
|
||||
/// The item count limit of the memory storage.
|
||||
public var countLimit: Int = .max
|
||||
|
||||
/// The `StorageExpiration` used in this memory storage. Default is `.seconds(300)`,
|
||||
/// means that the memory cache would expire in 5 minutes.
|
||||
public var expiration: StorageExpiration = .seconds(300)
|
||||
|
||||
/// The time interval between the storage do clean work for swiping expired items.
|
||||
public var cleanInterval: TimeInterval
|
||||
|
||||
/// Whether the newly added items to memory cache should be purged when the app goes to background.
|
||||
///
|
||||
/// By default, the cached items in memory will be purged as soon as the app goes to background to ensure
|
||||
/// least memory footprint. Enabling this would prevent this behavior and keep the items alive in cache even
|
||||
/// when your app is not in foreground anymore.
|
||||
///
|
||||
/// Default is `false`. After setting `true`, only the newly added cache objects are affected. Existing
|
||||
/// objects which are already in the cache while this value was `false` will be still be purged when entering
|
||||
/// background.
|
||||
public var keepWhenEnteringBackground: Bool = false
|
||||
|
||||
/// Creates a config from a given `totalCostLimit` value.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - totalCostLimit: Total cost limit of the storage in bytes.
|
||||
/// - cleanInterval: The time interval between the storage do clean work for swiping expired items.
|
||||
/// Default is 120, means the auto eviction happens once per two minutes.
|
||||
///
|
||||
/// - Note:
|
||||
/// Other members of `MemoryStorage.Config` will use their default values when created.
|
||||
public init(totalCostLimit: Int, cleanInterval: TimeInterval = 120) {
|
||||
self.totalCostLimit = totalCostLimit
|
||||
self.cleanInterval = cleanInterval
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MemoryStorage {
|
||||
|
||||
class BackgroundKeepingStorageObject<T>: StorageObject<T>, NSDiscardableContent {
|
||||
var accessing = true
|
||||
func beginContentAccess() -> Bool {
|
||||
if value != nil {
|
||||
accessing = true
|
||||
} else {
|
||||
accessing = false
|
||||
}
|
||||
return accessing
|
||||
}
|
||||
|
||||
func endContentAccess() {
|
||||
accessing = false
|
||||
}
|
||||
|
||||
func discardContentIfPossible() {
|
||||
value = nil
|
||||
}
|
||||
|
||||
func isContentDiscarded() -> Bool {
|
||||
return value == nil
|
||||
}
|
||||
}
|
||||
|
||||
class StorageObject<T> {
|
||||
var value: T?
|
||||
let expiration: StorageExpiration
|
||||
|
||||
private(set) var estimatedExpiration: Date
|
||||
|
||||
init(_ value: T, expiration: StorageExpiration) {
|
||||
self.value = value
|
||||
self.expiration = expiration
|
||||
|
||||
self.estimatedExpiration = expiration.estimatedExpirationSinceNow
|
||||
}
|
||||
|
||||
func extendExpiration(_ extendingExpiration: ExpirationExtending = .cacheTime) {
|
||||
switch extendingExpiration {
|
||||
case .none:
|
||||
return
|
||||
case .cacheTime:
|
||||
self.estimatedExpiration = expiration.estimatedExpirationSinceNow
|
||||
case .expirationTime(let expirationTime):
|
||||
self.estimatedExpiration = expirationTime.estimatedExpirationSinceNow
|
||||
}
|
||||
}
|
||||
|
||||
var isExpired: Bool {
|
||||
return estimatedExpiration.isPast
|
||||
}
|
||||
}
|
||||
}
|
||||
110
Pods/Kingfisher/Sources/Cache/Storage.swift
generated
Normal file
110
Pods/Kingfisher/Sources/Cache/Storage.swift
generated
Normal file
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// Storage.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 2018/10/15.
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Constants for some time intervals
|
||||
struct TimeConstants {
|
||||
static let secondsInOneDay = 86_400
|
||||
}
|
||||
|
||||
/// Represents the expiration strategy used in storage.
|
||||
///
|
||||
/// - never: The item never expires.
|
||||
/// - seconds: The item expires after a time duration of given seconds from now.
|
||||
/// - days: The item expires after a time duration of given days from now.
|
||||
/// - date: The item expires after a given date.
|
||||
public enum StorageExpiration {
|
||||
/// The item never expires.
|
||||
case never
|
||||
/// The item expires after a time duration of given seconds from now.
|
||||
case seconds(TimeInterval)
|
||||
/// The item expires after a time duration of given days from now.
|
||||
case days(Int)
|
||||
/// The item expires after a given date.
|
||||
case date(Date)
|
||||
/// Indicates the item is already expired. Use this to skip cache.
|
||||
case expired
|
||||
|
||||
func estimatedExpirationSince(_ date: Date) -> Date {
|
||||
switch self {
|
||||
case .never: return .distantFuture
|
||||
case .seconds(let seconds):
|
||||
return date.addingTimeInterval(seconds)
|
||||
case .days(let days):
|
||||
let duration: TimeInterval = TimeInterval(TimeConstants.secondsInOneDay) * TimeInterval(days)
|
||||
return date.addingTimeInterval(duration)
|
||||
case .date(let ref):
|
||||
return ref
|
||||
case .expired:
|
||||
return .distantPast
|
||||
}
|
||||
}
|
||||
|
||||
var estimatedExpirationSinceNow: Date {
|
||||
return estimatedExpirationSince(Date())
|
||||
}
|
||||
|
||||
var isExpired: Bool {
|
||||
return timeInterval <= 0
|
||||
}
|
||||
|
||||
var timeInterval: TimeInterval {
|
||||
switch self {
|
||||
case .never: return .infinity
|
||||
case .seconds(let seconds): return seconds
|
||||
case .days(let days): return TimeInterval(TimeConstants.secondsInOneDay) * TimeInterval(days)
|
||||
case .date(let ref): return ref.timeIntervalSinceNow
|
||||
case .expired: return -(.infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the expiration extending strategy used in storage to after access.
|
||||
///
|
||||
/// - none: The item expires after the original time, without extending after access.
|
||||
/// - cacheTime: The item expiration extends by the original cache time after each access.
|
||||
/// - expirationTime: The item expiration extends by the provided time after each access.
|
||||
public enum ExpirationExtending {
|
||||
/// The item expires after the original time, without extending after access.
|
||||
case none
|
||||
/// The item expiration extends by the original cache time after each access.
|
||||
case cacheTime
|
||||
/// The item expiration extends by the provided time after each access.
|
||||
case expirationTime(_ expiration: StorageExpiration)
|
||||
}
|
||||
|
||||
/// Represents types which cost in memory can be calculated.
|
||||
public protocol CacheCostCalculable {
|
||||
var cacheCost: Int { get }
|
||||
}
|
||||
|
||||
/// Represents types which can be converted to and from data.
|
||||
public protocol DataTransformable {
|
||||
func toData() throws -> Data
|
||||
static func fromData(_ data: Data) throws -> Self
|
||||
static var empty: Self { get }
|
||||
}
|
||||
245
Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift
generated
Normal file
245
Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift
generated
Normal file
@@ -0,0 +1,245 @@
|
||||
|
||||
//
|
||||
// CPListItem+Kingfisher.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wayne Hartman on 2021-08-29.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#if canImport(CarPlay) && !targetEnvironment(macCatalyst)
|
||||
import CarPlay
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
extension KingfisherWrapper where Base: CPListItem {
|
||||
|
||||
// MARK: Setting Image
|
||||
|
||||
/// Sets an image to the image view with a source.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - source: The `Source` object contains information about the image.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
///
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested source
|
||||
/// Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with source: Source?,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? []))
|
||||
return setImage(
|
||||
with: source,
|
||||
placeholder: placeholder,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets an image to the image view with a requested resource.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - resource: The `Resource` object contains information about the image.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
///
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
|
||||
/// or network. Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with resource: Resource?,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
return setImage(
|
||||
with: resource?.convertToSource(),
|
||||
placeholder: placeholder,
|
||||
options: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func setImage(
|
||||
with source: Source?,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
parsedOptions: KingfisherParsedOptionsInfo,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
var mutatingSelf = self
|
||||
guard let source = source else {
|
||||
/**
|
||||
* In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5
|
||||
* to allow `nil`. The compiler version 5.4 was introduced in this same SDK,
|
||||
* which allows >=14.5 SDK to set a `nil` image. This compile check allows
|
||||
* newer SDK users to set the image to `nil`, while still allowing older SDK
|
||||
* users to compile the framework.
|
||||
*/
|
||||
#if compiler(>=5.4)
|
||||
self.base.setImage(placeholder)
|
||||
#else
|
||||
if let placeholder = placeholder {
|
||||
self.base.setImage(placeholder)
|
||||
}
|
||||
#endif
|
||||
|
||||
mutatingSelf.taskIdentifier = nil
|
||||
completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
|
||||
return nil
|
||||
}
|
||||
|
||||
var options = parsedOptions
|
||||
if !options.keepCurrentImageWhileLoading {
|
||||
/**
|
||||
* In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5
|
||||
* to allow `nil`. The compiler version 5.4 was introduced in this same SDK,
|
||||
* which allows >=14.5 SDK to set a `nil` image. This compile check allows
|
||||
* newer SDK users to set the image to `nil`, while still allowing older SDK
|
||||
* users to compile the framework.
|
||||
*/
|
||||
#if compiler(>=5.4)
|
||||
self.base.setImage(placeholder)
|
||||
#else // Let older SDK users deal with the older behavior.
|
||||
if let placeholder = placeholder {
|
||||
self.base.setImage(placeholder)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
let issuedIdentifier = Source.Identifier.next()
|
||||
mutatingSelf.taskIdentifier = issuedIdentifier
|
||||
|
||||
if let block = progressBlock {
|
||||
options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
|
||||
}
|
||||
|
||||
let task = KingfisherManager.shared.retrieveImage(
|
||||
with: source,
|
||||
options: options,
|
||||
downloadTaskUpdated: { mutatingSelf.imageTask = $0 },
|
||||
progressiveImageSetter: { self.base.setImage($0) },
|
||||
referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier },
|
||||
completionHandler: { result in
|
||||
CallbackQueue.mainCurrentOrAsync.execute {
|
||||
guard issuedIdentifier == self.taskIdentifier else {
|
||||
let reason: KingfisherError.ImageSettingErrorReason
|
||||
do {
|
||||
let value = try result.get()
|
||||
reason = .notCurrentSourceTask(result: value, error: nil, source: source)
|
||||
} catch {
|
||||
reason = .notCurrentSourceTask(result: nil, error: error, source: source)
|
||||
}
|
||||
let error = KingfisherError.imageSettingError(reason: reason)
|
||||
completionHandler?(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
mutatingSelf.imageTask = nil
|
||||
mutatingSelf.taskIdentifier = nil
|
||||
|
||||
switch result {
|
||||
case .success(let value):
|
||||
self.base.setImage(value.image)
|
||||
completionHandler?(result)
|
||||
|
||||
case .failure:
|
||||
if let image = options.onFailureImage {
|
||||
/**
|
||||
* In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5
|
||||
* to allow `nil`. The compiler version 5.4 was introduced in this same SDK,
|
||||
* which allows >=14.5 SDK to set a `nil` image. This compile check allows
|
||||
* newer SDK users to set the image to `nil`, while still allowing older SDK
|
||||
* users to compile the framework.
|
||||
*/
|
||||
#if compiler(>=5.4)
|
||||
self.base.setImage(image)
|
||||
#else // Let older SDK users deal with the older behavior.
|
||||
if let unwrapped = image {
|
||||
self.base.setImage(unwrapped)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
completionHandler?(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
mutatingSelf.imageTask = task
|
||||
return task
|
||||
}
|
||||
|
||||
// MARK: Cancelling Image
|
||||
|
||||
/// Cancel the image download task bounded to the image view if it is running.
|
||||
/// Nothing will happen if the downloading has already finished.
|
||||
public func cancelDownloadTask() {
|
||||
imageTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private var taskIdentifierKey: Void?
|
||||
private var imageTaskKey: Void?
|
||||
|
||||
// MARK: Properties
|
||||
extension KingfisherWrapper where Base: CPListItem {
|
||||
|
||||
public private(set) var taskIdentifier: Source.Identifier.Value? {
|
||||
get {
|
||||
let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)
|
||||
return box?.value
|
||||
}
|
||||
set {
|
||||
let box = newValue.map { Box($0) }
|
||||
setRetainedAssociatedObject(base, &taskIdentifierKey, box)
|
||||
}
|
||||
}
|
||||
|
||||
private var imageTask: DownloadTask? {
|
||||
get { return getAssociatedObject(base, &imageTaskKey) }
|
||||
set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
537
Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift
generated
Normal file
537
Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift
generated
Normal file
@@ -0,0 +1,537 @@
|
||||
//
|
||||
// ImageView+Kingfisher.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.
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
extension KingfisherWrapper where Base: KFCrossPlatformImageView {
|
||||
|
||||
// MARK: Setting Image
|
||||
|
||||
/// Sets an image to the image view with a `Source`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - source: The `Source` object defines data information from network or a data provider.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
/// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters
|
||||
/// have a default value except the `source`, you can set an image from a certain URL to an image view like this:
|
||||
///
|
||||
/// ```
|
||||
/// // Set image from a network source.
|
||||
/// let url = URL(string: "https://example.com/image.png")!
|
||||
/// imageView.kf.setImage(with: .network(url))
|
||||
///
|
||||
/// // Or set image from a data provider.
|
||||
/// let provider = LocalFileImageDataProvider(fileURL: fileURL)
|
||||
/// imageView.kf.setImage(with: .provider(provider))
|
||||
/// ```
|
||||
///
|
||||
/// For both `.network` and `.provider` source, there are corresponding view extension methods. So the code
|
||||
/// above is equivalent to:
|
||||
///
|
||||
/// ```
|
||||
/// imageView.kf.setImage(with: url)
|
||||
/// imageView.kf.setImage(with: provider)
|
||||
/// ```
|
||||
///
|
||||
/// Internally, this method will use `KingfisherManager` to get the source.
|
||||
/// Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with source: Source?,
|
||||
placeholder: Placeholder? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
|
||||
return setImage(with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/// Sets an image to the image view with a `Source`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - source: The `Source` object defines data information from network or a data provider.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
/// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters
|
||||
/// have a default value except the `source`, you can set an image from a certain URL to an image view like this:
|
||||
///
|
||||
/// ```
|
||||
/// // Set image from a network source.
|
||||
/// let url = URL(string: "https://example.com/image.png")!
|
||||
/// imageView.kf.setImage(with: .network(url))
|
||||
///
|
||||
/// // Or set image from a data provider.
|
||||
/// let provider = LocalFileImageDataProvider(fileURL: fileURL)
|
||||
/// imageView.kf.setImage(with: .provider(provider))
|
||||
/// ```
|
||||
///
|
||||
/// For both `.network` and `.provider` source, there are corresponding view extension methods. So the code
|
||||
/// above is equivalent to:
|
||||
///
|
||||
/// ```
|
||||
/// imageView.kf.setImage(with: url)
|
||||
/// imageView.kf.setImage(with: provider)
|
||||
/// ```
|
||||
///
|
||||
/// Internally, this method will use `KingfisherManager` to get the source.
|
||||
/// Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// The `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with source: Source?,
|
||||
placeholder: Placeholder? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
return setImage(
|
||||
with: source,
|
||||
placeholder: placeholder,
|
||||
options: options,
|
||||
progressBlock: nil,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets an image to the image view with a requested resource.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - resource: The `Resource` object contains information about the resource.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
/// This is the easiest way to use Kingfisher to boost the image setting process from network. Since all parameters
|
||||
/// have a default value except the `resource`, you can set an image from a certain URL to an image view like this:
|
||||
///
|
||||
/// ```
|
||||
/// let url = URL(string: "https://example.com/image.png")!
|
||||
/// imageView.kf.setImage(with: url)
|
||||
/// ```
|
||||
///
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
|
||||
/// or network. Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with resource: Resource?,
|
||||
placeholder: Placeholder? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
return setImage(
|
||||
with: resource?.convertToSource(),
|
||||
placeholder: placeholder,
|
||||
options: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/// Sets an image to the image view with a requested resource.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - resource: The `Resource` object contains information about the resource.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
/// This is the easiest way to use Kingfisher to boost the image setting process from network. Since all parameters
|
||||
/// have a default value except the `resource`, you can set an image from a certain URL to an image view like this:
|
||||
///
|
||||
/// ```
|
||||
/// let url = URL(string: "https://example.com/image.png")!
|
||||
/// imageView.kf.setImage(with: url)
|
||||
/// ```
|
||||
///
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
|
||||
/// or network. Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// The `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with resource: Resource?,
|
||||
placeholder: Placeholder? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
return setImage(
|
||||
with: resource,
|
||||
placeholder: placeholder,
|
||||
options: options,
|
||||
progressBlock: nil,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets an image to the image view with a data provider.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - provider: The `ImageDataProvider` object contains information about the data.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// Internally, this method will use `KingfisherManager` to get the image data, from either cache
|
||||
/// or the data provider. Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with provider: ImageDataProvider?,
|
||||
placeholder: Placeholder? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
return setImage(
|
||||
with: provider.map { .provider($0) },
|
||||
placeholder: placeholder,
|
||||
options: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/// Sets an image to the image view with a data provider.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - provider: The `ImageDataProvider` object contains information about the data.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// Internally, this method will use `KingfisherManager` to get the image data, from either cache
|
||||
/// or the data provider. Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// The `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with provider: ImageDataProvider?,
|
||||
placeholder: Placeholder? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
return setImage(
|
||||
with: provider,
|
||||
placeholder: placeholder,
|
||||
options: options,
|
||||
progressBlock: nil,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
func setImage(
|
||||
with source: Source?,
|
||||
placeholder: Placeholder? = nil,
|
||||
parsedOptions: KingfisherParsedOptionsInfo,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
var mutatingSelf = self
|
||||
guard let source = source else {
|
||||
mutatingSelf.placeholder = placeholder
|
||||
mutatingSelf.taskIdentifier = nil
|
||||
completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
|
||||
return nil
|
||||
}
|
||||
|
||||
var options = parsedOptions
|
||||
|
||||
let isEmptyImage = base.image == nil && self.placeholder == nil
|
||||
if !options.keepCurrentImageWhileLoading || isEmptyImage {
|
||||
// Always set placeholder while there is no image/placeholder yet.
|
||||
mutatingSelf.placeholder = placeholder
|
||||
}
|
||||
|
||||
let maybeIndicator = indicator
|
||||
maybeIndicator?.startAnimatingView()
|
||||
|
||||
let issuedIdentifier = Source.Identifier.next()
|
||||
mutatingSelf.taskIdentifier = issuedIdentifier
|
||||
|
||||
if base.shouldPreloadAllAnimation() {
|
||||
options.preloadAllAnimationData = true
|
||||
}
|
||||
|
||||
if let block = progressBlock {
|
||||
options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
|
||||
}
|
||||
|
||||
let task = KingfisherManager.shared.retrieveImage(
|
||||
with: source,
|
||||
options: options,
|
||||
downloadTaskUpdated: { mutatingSelf.imageTask = $0 },
|
||||
progressiveImageSetter: { self.base.image = $0 },
|
||||
referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier },
|
||||
completionHandler: { result in
|
||||
CallbackQueue.mainCurrentOrAsync.execute {
|
||||
maybeIndicator?.stopAnimatingView()
|
||||
guard issuedIdentifier == self.taskIdentifier else {
|
||||
let reason: KingfisherError.ImageSettingErrorReason
|
||||
do {
|
||||
let value = try result.get()
|
||||
reason = .notCurrentSourceTask(result: value, error: nil, source: source)
|
||||
} catch {
|
||||
reason = .notCurrentSourceTask(result: nil, error: error, source: source)
|
||||
}
|
||||
let error = KingfisherError.imageSettingError(reason: reason)
|
||||
completionHandler?(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
mutatingSelf.imageTask = nil
|
||||
mutatingSelf.taskIdentifier = nil
|
||||
|
||||
switch result {
|
||||
case .success(let value):
|
||||
guard self.needsTransition(options: options, cacheType: value.cacheType) else {
|
||||
mutatingSelf.placeholder = nil
|
||||
self.base.image = value.image
|
||||
completionHandler?(result)
|
||||
return
|
||||
}
|
||||
|
||||
self.makeTransition(image: value.image, transition: options.transition) {
|
||||
completionHandler?(result)
|
||||
}
|
||||
|
||||
case .failure:
|
||||
if let image = options.onFailureImage {
|
||||
mutatingSelf.placeholder = nil
|
||||
self.base.image = image
|
||||
}
|
||||
completionHandler?(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
mutatingSelf.imageTask = task
|
||||
return task
|
||||
}
|
||||
|
||||
// MARK: Cancelling Downloading Task
|
||||
|
||||
/// Cancels the image download task of the image view if it is running.
|
||||
/// Nothing will happen if the downloading has already finished.
|
||||
public func cancelDownloadTask() {
|
||||
imageTask?.cancel()
|
||||
}
|
||||
|
||||
private func needsTransition(options: KingfisherParsedOptionsInfo, cacheType: CacheType) -> Bool {
|
||||
switch options.transition {
|
||||
case .none:
|
||||
return false
|
||||
#if os(macOS)
|
||||
case .fade: // Fade is only a placeholder for SwiftUI on macOS.
|
||||
return false
|
||||
#else
|
||||
default:
|
||||
if options.forceTransition { return true }
|
||||
if cacheType == .none { return true }
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private func makeTransition(image: KFCrossPlatformImage, transition: ImageTransition, done: @escaping () -> Void) {
|
||||
#if !os(macOS)
|
||||
// Force hiding the indicator without transition first.
|
||||
UIView.transition(
|
||||
with: self.base,
|
||||
duration: 0.0,
|
||||
options: [],
|
||||
animations: { self.indicator?.stopAnimatingView() },
|
||||
completion: { _ in
|
||||
var mutatingSelf = self
|
||||
mutatingSelf.placeholder = nil
|
||||
UIView.transition(
|
||||
with: self.base,
|
||||
duration: transition.duration,
|
||||
options: [transition.animationOptions, .allowUserInteraction],
|
||||
animations: { transition.animations?(self.base, image) },
|
||||
completion: { finished in
|
||||
transition.completion?(finished)
|
||||
done()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
#else
|
||||
done()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Associated Object
|
||||
private var taskIdentifierKey: Void?
|
||||
private var indicatorKey: Void?
|
||||
private var indicatorTypeKey: Void?
|
||||
private var placeholderKey: Void?
|
||||
private var imageTaskKey: Void?
|
||||
|
||||
extension KingfisherWrapper where Base: KFCrossPlatformImageView {
|
||||
|
||||
// MARK: Properties
|
||||
public private(set) var taskIdentifier: Source.Identifier.Value? {
|
||||
get {
|
||||
let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)
|
||||
return box?.value
|
||||
}
|
||||
set {
|
||||
let box = newValue.map { Box($0) }
|
||||
setRetainedAssociatedObject(base, &taskIdentifierKey, box)
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds which indicator type is going to be used.
|
||||
/// Default is `.none`, means no indicator will be shown while downloading.
|
||||
public var indicatorType: IndicatorType {
|
||||
get {
|
||||
return getAssociatedObject(base, &indicatorTypeKey) ?? .none
|
||||
}
|
||||
|
||||
set {
|
||||
switch newValue {
|
||||
case .none: indicator = nil
|
||||
case .activity: indicator = ActivityIndicator()
|
||||
case .image(let data): indicator = ImageIndicator(imageData: data)
|
||||
case .custom(let anIndicator): indicator = anIndicator
|
||||
}
|
||||
|
||||
setRetainedAssociatedObject(base, &indicatorTypeKey, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds any type that conforms to the protocol `Indicator`.
|
||||
/// The protocol `Indicator` has a `view` property that will be shown when loading an image.
|
||||
/// It will be `nil` if `indicatorType` is `.none`.
|
||||
public private(set) var indicator: Indicator? {
|
||||
get {
|
||||
let box: Box<Indicator>? = getAssociatedObject(base, &indicatorKey)
|
||||
return box?.value
|
||||
}
|
||||
|
||||
set {
|
||||
// Remove previous
|
||||
if let previousIndicator = indicator {
|
||||
previousIndicator.view.removeFromSuperview()
|
||||
}
|
||||
|
||||
// Add new
|
||||
if let newIndicator = newValue {
|
||||
// Set default indicator layout
|
||||
let view = newIndicator.view
|
||||
|
||||
base.addSubview(view)
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.centerXAnchor.constraint(
|
||||
equalTo: base.centerXAnchor, constant: newIndicator.centerOffset.x).isActive = true
|
||||
view.centerYAnchor.constraint(
|
||||
equalTo: base.centerYAnchor, constant: newIndicator.centerOffset.y).isActive = true
|
||||
|
||||
switch newIndicator.sizeStrategy(in: base) {
|
||||
case .intrinsicSize:
|
||||
break
|
||||
case .full:
|
||||
view.heightAnchor.constraint(equalTo: base.heightAnchor, constant: 0).isActive = true
|
||||
view.widthAnchor.constraint(equalTo: base.widthAnchor, constant: 0).isActive = true
|
||||
case .size(let size):
|
||||
view.heightAnchor.constraint(equalToConstant: size.height).isActive = true
|
||||
view.widthAnchor.constraint(equalToConstant: size.width).isActive = true
|
||||
}
|
||||
|
||||
newIndicator.view.isHidden = true
|
||||
}
|
||||
|
||||
// Save in associated object
|
||||
// Wrap newValue with Box to workaround an issue that Swift does not recognize
|
||||
// and casting protocol for associate object correctly. https://github.com/onevcat/Kingfisher/issues/872
|
||||
setRetainedAssociatedObject(base, &indicatorKey, newValue.map(Box.init))
|
||||
}
|
||||
}
|
||||
|
||||
private var imageTask: DownloadTask? {
|
||||
get { return getAssociatedObject(base, &imageTaskKey) }
|
||||
set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}
|
||||
}
|
||||
|
||||
/// Represents the `Placeholder` used for this image view. A `Placeholder` will be shown in the view while
|
||||
/// it is downloading an image.
|
||||
public private(set) var placeholder: Placeholder? {
|
||||
get { return getAssociatedObject(base, &placeholderKey) }
|
||||
set {
|
||||
if let previousPlaceholder = placeholder {
|
||||
previousPlaceholder.remove(from: base)
|
||||
}
|
||||
|
||||
if let newPlaceholder = newValue {
|
||||
newPlaceholder.add(to: base)
|
||||
} else {
|
||||
base.image = nil
|
||||
}
|
||||
setRetainedAssociatedObject(base, &placeholderKey, newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension KFCrossPlatformImageView {
|
||||
@objc func shouldPreloadAllAnimation() -> Bool { return true }
|
||||
}
|
||||
|
||||
#endif
|
||||
362
Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift
generated
Normal file
362
Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift
generated
Normal file
@@ -0,0 +1,362 @@
|
||||
//
|
||||
// NSButton+Kingfisher.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Jie Zhang on 14/04/2016.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
|
||||
|
||||
import AppKit
|
||||
|
||||
extension KingfisherWrapper where Base: NSButton {
|
||||
|
||||
// MARK: Setting Image
|
||||
|
||||
/// Sets an image to the button with a source.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - source: The `Source` object contains information about how to get the image.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested source.
|
||||
/// Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with source: Source?,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
|
||||
return setImage(
|
||||
with: source,
|
||||
placeholder: placeholder,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets an image to the button with a requested resource.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - resource: The `Resource` object contains information about the resource.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
|
||||
/// or network. Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with resource: Resource?,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
return setImage(
|
||||
with: resource?.convertToSource(),
|
||||
placeholder: placeholder,
|
||||
options: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func setImage(
|
||||
with source: Source?,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
parsedOptions: KingfisherParsedOptionsInfo,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
var mutatingSelf = self
|
||||
guard let source = source else {
|
||||
base.image = placeholder
|
||||
mutatingSelf.taskIdentifier = nil
|
||||
completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
|
||||
return nil
|
||||
}
|
||||
|
||||
var options = parsedOptions
|
||||
if !options.keepCurrentImageWhileLoading {
|
||||
base.image = placeholder
|
||||
}
|
||||
|
||||
let issuedIdentifier = Source.Identifier.next()
|
||||
mutatingSelf.taskIdentifier = issuedIdentifier
|
||||
|
||||
if let block = progressBlock {
|
||||
options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
|
||||
}
|
||||
|
||||
let task = KingfisherManager.shared.retrieveImage(
|
||||
with: source,
|
||||
options: options,
|
||||
downloadTaskUpdated: { mutatingSelf.imageTask = $0 },
|
||||
progressiveImageSetter: { self.base.image = $0 },
|
||||
referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier },
|
||||
completionHandler: { result in
|
||||
CallbackQueue.mainCurrentOrAsync.execute {
|
||||
guard issuedIdentifier == self.taskIdentifier else {
|
||||
let reason: KingfisherError.ImageSettingErrorReason
|
||||
do {
|
||||
let value = try result.get()
|
||||
reason = .notCurrentSourceTask(result: value, error: nil, source: source)
|
||||
} catch {
|
||||
reason = .notCurrentSourceTask(result: nil, error: error, source: source)
|
||||
}
|
||||
let error = KingfisherError.imageSettingError(reason: reason)
|
||||
completionHandler?(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
mutatingSelf.imageTask = nil
|
||||
mutatingSelf.taskIdentifier = nil
|
||||
|
||||
switch result {
|
||||
case .success(let value):
|
||||
self.base.image = value.image
|
||||
completionHandler?(result)
|
||||
|
||||
case .failure:
|
||||
if let image = options.onFailureImage {
|
||||
self.base.image = image
|
||||
}
|
||||
completionHandler?(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
mutatingSelf.imageTask = task
|
||||
return task
|
||||
}
|
||||
|
||||
// MARK: Cancelling Downloading Task
|
||||
|
||||
/// Cancels the image download task of the button if it is running.
|
||||
/// Nothing will happen if the downloading has already finished.
|
||||
public func cancelImageDownloadTask() {
|
||||
imageTask?.cancel()
|
||||
}
|
||||
|
||||
// MARK: Setting Alternate Image
|
||||
|
||||
@discardableResult
|
||||
public func setAlternateImage(
|
||||
with source: Source?,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
|
||||
return setAlternateImage(
|
||||
with: source,
|
||||
placeholder: placeholder,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets an alternate image to the button with a requested resource.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - resource: The `Resource` object contains information about the resource.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
|
||||
/// or network. Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setAlternateImage(
|
||||
with resource: Resource?,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
return setAlternateImage(
|
||||
with: resource?.convertToSource(),
|
||||
placeholder: placeholder,
|
||||
options: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func setAlternateImage(
|
||||
with source: Source?,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
parsedOptions: KingfisherParsedOptionsInfo,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
var mutatingSelf = self
|
||||
guard let source = source else {
|
||||
base.alternateImage = placeholder
|
||||
mutatingSelf.alternateTaskIdentifier = nil
|
||||
completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
|
||||
return nil
|
||||
}
|
||||
|
||||
var options = parsedOptions
|
||||
if !options.keepCurrentImageWhileLoading {
|
||||
base.alternateImage = placeholder
|
||||
}
|
||||
|
||||
let issuedIdentifier = Source.Identifier.next()
|
||||
mutatingSelf.alternateTaskIdentifier = issuedIdentifier
|
||||
|
||||
if let block = progressBlock {
|
||||
options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
|
||||
}
|
||||
|
||||
if let provider = ImageProgressiveProvider(options, refresh: { image in
|
||||
self.base.alternateImage = image
|
||||
}) {
|
||||
options.onDataReceived = (options.onDataReceived ?? []) + [provider]
|
||||
}
|
||||
|
||||
options.onDataReceived?.forEach {
|
||||
$0.onShouldApply = { issuedIdentifier == self.alternateTaskIdentifier }
|
||||
}
|
||||
|
||||
let task = KingfisherManager.shared.retrieveImage(
|
||||
with: source,
|
||||
options: options,
|
||||
downloadTaskUpdated: { mutatingSelf.alternateImageTask = $0 },
|
||||
completionHandler: { result in
|
||||
CallbackQueue.mainCurrentOrAsync.execute {
|
||||
guard issuedIdentifier == self.alternateTaskIdentifier else {
|
||||
let reason: KingfisherError.ImageSettingErrorReason
|
||||
do {
|
||||
let value = try result.get()
|
||||
reason = .notCurrentSourceTask(result: value, error: nil, source: source)
|
||||
} catch {
|
||||
reason = .notCurrentSourceTask(result: nil, error: error, source: source)
|
||||
}
|
||||
let error = KingfisherError.imageSettingError(reason: reason)
|
||||
completionHandler?(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
mutatingSelf.alternateImageTask = nil
|
||||
mutatingSelf.alternateTaskIdentifier = nil
|
||||
|
||||
switch result {
|
||||
case .success(let value):
|
||||
self.base.alternateImage = value.image
|
||||
completionHandler?(result)
|
||||
|
||||
case .failure:
|
||||
if let image = options.onFailureImage {
|
||||
self.base.alternateImage = image
|
||||
}
|
||||
completionHandler?(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
mutatingSelf.alternateImageTask = task
|
||||
return task
|
||||
}
|
||||
|
||||
// MARK: Cancelling Alternate Image Downloading Task
|
||||
|
||||
/// Cancels the alternate image download task of the button if it is running.
|
||||
/// Nothing will happen if the downloading has already finished.
|
||||
public func cancelAlternateImageDownloadTask() {
|
||||
alternateImageTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Associated Object
|
||||
private var taskIdentifierKey: Void?
|
||||
private var imageTaskKey: Void?
|
||||
|
||||
private var alternateTaskIdentifierKey: Void?
|
||||
private var alternateImageTaskKey: Void?
|
||||
|
||||
extension KingfisherWrapper where Base: NSButton {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
public private(set) var taskIdentifier: Source.Identifier.Value? {
|
||||
get {
|
||||
let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)
|
||||
return box?.value
|
||||
}
|
||||
set {
|
||||
let box = newValue.map { Box($0) }
|
||||
setRetainedAssociatedObject(base, &taskIdentifierKey, box)
|
||||
}
|
||||
}
|
||||
|
||||
private var imageTask: DownloadTask? {
|
||||
get { return getAssociatedObject(base, &imageTaskKey) }
|
||||
set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}
|
||||
}
|
||||
|
||||
public private(set) var alternateTaskIdentifier: Source.Identifier.Value? {
|
||||
get {
|
||||
let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &alternateTaskIdentifierKey)
|
||||
return box?.value
|
||||
}
|
||||
set {
|
||||
let box = newValue.map { Box($0) }
|
||||
setRetainedAssociatedObject(base, &alternateTaskIdentifierKey, box)
|
||||
}
|
||||
}
|
||||
|
||||
private var alternateImageTask: DownloadTask? {
|
||||
get { return getAssociatedObject(base, &alternateImageTaskKey) }
|
||||
set { setRetainedAssociatedObject(base, &alternateImageTaskKey, newValue)}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
271
Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift
generated
Normal file
271
Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift
generated
Normal file
@@ -0,0 +1,271 @@
|
||||
//
|
||||
// NSTextAttachment+Kingfisher.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Benjamin Briggs on 22/07/2019.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
extension KingfisherWrapper where Base: NSTextAttachment {
|
||||
|
||||
// MARK: Setting Image
|
||||
|
||||
/// Sets an image to the text attachment with a source.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - source: The `Source` object defines data information from network or a data provider.
|
||||
/// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
///
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested source
|
||||
/// Since this method will perform UI changes, you must call it from the main thread.
|
||||
///
|
||||
/// The retrieved image will be set to `NSTextAttachment.image` property. Because it is not an image view based
|
||||
/// rendering, options related to view, such as `.transition`, are not supported.
|
||||
///
|
||||
/// Kingfisher will call `setNeedsDisplay` on the `attributedView` when the image task done. It gives the view a
|
||||
/// chance to render the attributed string again for displaying the downloaded image. For example, if you set an
|
||||
/// attributed with this `NSTextAttachment` to a `UILabel` object, pass it as the `attributedView` parameter.
|
||||
///
|
||||
/// Here is a typical use case:
|
||||
///
|
||||
/// ```swift
|
||||
/// let attributedText = NSMutableAttributedString(string: "Hello World")
|
||||
/// let textAttachment = NSTextAttachment()
|
||||
///
|
||||
/// textAttachment.kf.setImage(
|
||||
/// with: URL(string: "https://onevcat.com/assets/images/avatar.jpg")!,
|
||||
/// attributedView: label,
|
||||
/// options: [
|
||||
/// .processor(
|
||||
/// ResizingImageProcessor(referenceSize: .init(width: 30, height: 30))
|
||||
/// |> RoundCornerImageProcessor(cornerRadius: 15))
|
||||
/// ]
|
||||
/// )
|
||||
/// attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment))
|
||||
/// label.attributedText = attributedText
|
||||
/// ```
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with source: Source?,
|
||||
attributedView: @autoclosure @escaping () -> KFCrossPlatformView,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
|
||||
return setImage(
|
||||
with: source,
|
||||
attributedView: attributedView,
|
||||
placeholder: placeholder,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets an image to the text attachment with a source.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - resource: The `Resource` object contains information about the resource.
|
||||
/// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
///
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested source
|
||||
/// Since this method will perform UI changes, you must call it from the main thread.
|
||||
///
|
||||
/// The retrieved image will be set to `NSTextAttachment.image` property. Because it is not an image view based
|
||||
/// rendering, options related to view, such as `.transition`, are not supported.
|
||||
///
|
||||
/// Kingfisher will call `setNeedsDisplay` on the `attributedView` when the image task done. It gives the view a
|
||||
/// chance to render the attributed string again for displaying the downloaded image. For example, if you set an
|
||||
/// attributed with this `NSTextAttachment` to a `UILabel` object, pass it as the `attributedView` parameter.
|
||||
///
|
||||
/// Here is a typical use case:
|
||||
///
|
||||
/// ```swift
|
||||
/// let attributedText = NSMutableAttributedString(string: "Hello World")
|
||||
/// let textAttachment = NSTextAttachment()
|
||||
///
|
||||
/// textAttachment.kf.setImage(
|
||||
/// with: URL(string: "https://onevcat.com/assets/images/avatar.jpg")!,
|
||||
/// attributedView: label,
|
||||
/// options: [
|
||||
/// .processor(
|
||||
/// ResizingImageProcessor(referenceSize: .init(width: 30, height: 30))
|
||||
/// |> RoundCornerImageProcessor(cornerRadius: 15))
|
||||
/// ]
|
||||
/// )
|
||||
/// attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment))
|
||||
/// label.attributedText = attributedText
|
||||
/// ```
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with resource: Resource?,
|
||||
attributedView: @autoclosure @escaping () -> KFCrossPlatformView,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
|
||||
return setImage(
|
||||
with: resource.map { .network($0) },
|
||||
attributedView: attributedView,
|
||||
placeholder: placeholder,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
|
||||
func setImage(
|
||||
with source: Source?,
|
||||
attributedView: @escaping () -> KFCrossPlatformView,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
parsedOptions: KingfisherParsedOptionsInfo,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
var mutatingSelf = self
|
||||
guard let source = source else {
|
||||
base.image = placeholder
|
||||
mutatingSelf.taskIdentifier = nil
|
||||
completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
|
||||
return nil
|
||||
}
|
||||
|
||||
var options = parsedOptions
|
||||
if !options.keepCurrentImageWhileLoading {
|
||||
base.image = placeholder
|
||||
}
|
||||
|
||||
let issuedIdentifier = Source.Identifier.next()
|
||||
mutatingSelf.taskIdentifier = issuedIdentifier
|
||||
|
||||
if let block = progressBlock {
|
||||
options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
|
||||
}
|
||||
|
||||
let task = KingfisherManager.shared.retrieveImage(
|
||||
with: source,
|
||||
options: options,
|
||||
progressiveImageSetter: { self.base.image = $0 },
|
||||
referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier },
|
||||
completionHandler: { result in
|
||||
CallbackQueue.mainCurrentOrAsync.execute {
|
||||
guard issuedIdentifier == self.taskIdentifier else {
|
||||
let reason: KingfisherError.ImageSettingErrorReason
|
||||
do {
|
||||
let value = try result.get()
|
||||
reason = .notCurrentSourceTask(result: value, error: nil, source: source)
|
||||
} catch {
|
||||
reason = .notCurrentSourceTask(result: nil, error: error, source: source)
|
||||
}
|
||||
let error = KingfisherError.imageSettingError(reason: reason)
|
||||
completionHandler?(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
mutatingSelf.imageTask = nil
|
||||
mutatingSelf.taskIdentifier = nil
|
||||
|
||||
switch result {
|
||||
case .success(let value):
|
||||
self.base.image = value.image
|
||||
let view = attributedView()
|
||||
#if canImport(UIKit)
|
||||
view.setNeedsDisplay()
|
||||
#else
|
||||
view.setNeedsDisplay(view.bounds)
|
||||
#endif
|
||||
case .failure:
|
||||
if let image = options.onFailureImage {
|
||||
self.base.image = image
|
||||
}
|
||||
}
|
||||
completionHandler?(result)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
mutatingSelf.imageTask = task
|
||||
return task
|
||||
}
|
||||
|
||||
// MARK: Cancelling Image
|
||||
|
||||
/// Cancel the image download task bounded to the text attachment if it is running.
|
||||
/// Nothing will happen if the downloading has already finished.
|
||||
public func cancelDownloadTask() {
|
||||
imageTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private var taskIdentifierKey: Void?
|
||||
private var imageTaskKey: Void?
|
||||
|
||||
// MARK: Properties
|
||||
extension KingfisherWrapper where Base: NSTextAttachment {
|
||||
|
||||
public private(set) var taskIdentifier: Source.Identifier.Value? {
|
||||
get {
|
||||
let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)
|
||||
return box?.value
|
||||
}
|
||||
set {
|
||||
let box = newValue.map { Box($0) }
|
||||
setRetainedAssociatedObject(base, &taskIdentifierKey, box)
|
||||
}
|
||||
}
|
||||
|
||||
private var imageTask: DownloadTask? {
|
||||
get { return getAssociatedObject(base, &imageTaskKey) }
|
||||
set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
209
Pods/Kingfisher/Sources/Extensions/TVMonogramView+Kingfisher.swift
generated
Normal file
209
Pods/Kingfisher/Sources/Extensions/TVMonogramView+Kingfisher.swift
generated
Normal file
@@ -0,0 +1,209 @@
|
||||
//
|
||||
// TVMonogramView+Kingfisher.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Marvin Nazari on 2020-12-07.
|
||||
//
|
||||
// Copyright (c) 2020 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 canImport(TVUIKit)
|
||||
|
||||
import TVUIKit
|
||||
|
||||
@available(tvOS 12.0, *)
|
||||
extension KingfisherWrapper where Base: TVMonogramView {
|
||||
|
||||
// MARK: Setting Image
|
||||
|
||||
/// Sets an image to the image view with a source.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - source: The `Source` object contains information about the image.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
///
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested source
|
||||
/// Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with source: Source?,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
|
||||
return setImage(
|
||||
with: source,
|
||||
placeholder: placeholder,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
|
||||
func setImage(
|
||||
with source: Source?,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
parsedOptions: KingfisherParsedOptionsInfo,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
var mutatingSelf = self
|
||||
guard let source = source else {
|
||||
base.image = placeholder
|
||||
mutatingSelf.taskIdentifier = nil
|
||||
completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
|
||||
return nil
|
||||
}
|
||||
|
||||
var options = parsedOptions
|
||||
if !options.keepCurrentImageWhileLoading {
|
||||
base.image = placeholder
|
||||
}
|
||||
|
||||
let issuedIdentifier = Source.Identifier.next()
|
||||
mutatingSelf.taskIdentifier = issuedIdentifier
|
||||
|
||||
if let block = progressBlock {
|
||||
options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
|
||||
}
|
||||
|
||||
let task = KingfisherManager.shared.retrieveImage(
|
||||
with: source,
|
||||
options: options,
|
||||
downloadTaskUpdated: { mutatingSelf.imageTask = $0 },
|
||||
progressiveImageSetter: { self.base.image = $0 },
|
||||
referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier },
|
||||
completionHandler: { result in
|
||||
CallbackQueue.mainCurrentOrAsync.execute {
|
||||
guard issuedIdentifier == self.taskIdentifier else {
|
||||
let reason: KingfisherError.ImageSettingErrorReason
|
||||
do {
|
||||
let value = try result.get()
|
||||
reason = .notCurrentSourceTask(result: value, error: nil, source: source)
|
||||
} catch {
|
||||
reason = .notCurrentSourceTask(result: nil, error: error, source: source)
|
||||
}
|
||||
let error = KingfisherError.imageSettingError(reason: reason)
|
||||
completionHandler?(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
mutatingSelf.imageTask = nil
|
||||
mutatingSelf.taskIdentifier = nil
|
||||
|
||||
switch result {
|
||||
case .success(let value):
|
||||
self.base.image = value.image
|
||||
completionHandler?(result)
|
||||
|
||||
case .failure:
|
||||
if let image = options.onFailureImage {
|
||||
self.base.image = image
|
||||
}
|
||||
completionHandler?(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
mutatingSelf.imageTask = task
|
||||
return task
|
||||
}
|
||||
|
||||
/// Sets an image to the image view with a requested resource.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - resource: The `Resource` object contains information about the image.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
///
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
|
||||
/// or network. Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with resource: Resource?,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
return setImage(
|
||||
with: resource?.convertToSource(),
|
||||
placeholder: placeholder,
|
||||
options: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
// MARK: Cancelling Image
|
||||
|
||||
/// Cancel the image download task bounded to the image view if it is running.
|
||||
/// Nothing will happen if the downloading has already finished.
|
||||
public func cancelDownloadTask() {
|
||||
imageTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private var taskIdentifierKey: Void?
|
||||
private var imageTaskKey: Void?
|
||||
|
||||
// MARK: Properties
|
||||
@available(tvOS 12.0, *)
|
||||
extension KingfisherWrapper where Base: TVMonogramView {
|
||||
|
||||
public private(set) var taskIdentifier: Source.Identifier.Value? {
|
||||
get {
|
||||
let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)
|
||||
return box?.value
|
||||
}
|
||||
set {
|
||||
let box = newValue.map { Box($0) }
|
||||
setRetainedAssociatedObject(base, &taskIdentifierKey, box)
|
||||
}
|
||||
}
|
||||
|
||||
private var imageTask: DownloadTask? {
|
||||
get { return getAssociatedObject(base, &imageTaskKey) }
|
||||
set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
400
Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift
generated
Normal file
400
Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift
generated
Normal file
@@ -0,0 +1,400 @@
|
||||
//
|
||||
// UIButton+Kingfisher.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 15/4/13.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
|
||||
extension KingfisherWrapper where Base: UIButton {
|
||||
|
||||
// MARK: Setting Image
|
||||
/// Sets an image to the button for a specified state with a source.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - source: The `Source` object contains information about the image.
|
||||
/// - state: The button state to which the image should be set.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested source, from either cache
|
||||
/// or network. Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with source: Source?,
|
||||
for state: UIControl.State,
|
||||
placeholder: UIImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
|
||||
return setImage(
|
||||
with: source,
|
||||
for: state,
|
||||
placeholder: placeholder,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets an image to the button for a specified state with a requested resource.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - resource: The `Resource` object contains information about the resource.
|
||||
/// - state: The button state to which the image should be set.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
|
||||
/// or network. Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with resource: Resource?,
|
||||
for state: UIControl.State,
|
||||
placeholder: UIImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
return setImage(
|
||||
with: resource?.convertToSource(),
|
||||
for: state,
|
||||
placeholder: placeholder,
|
||||
options: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with source: Source?,
|
||||
for state: UIControl.State,
|
||||
placeholder: UIImage? = nil,
|
||||
parsedOptions: KingfisherParsedOptionsInfo,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
guard let source = source else {
|
||||
base.setImage(placeholder, for: state)
|
||||
setTaskIdentifier(nil, for: state)
|
||||
completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
|
||||
return nil
|
||||
}
|
||||
|
||||
var options = parsedOptions
|
||||
if !options.keepCurrentImageWhileLoading {
|
||||
base.setImage(placeholder, for: state)
|
||||
}
|
||||
|
||||
var mutatingSelf = self
|
||||
let issuedIdentifier = Source.Identifier.next()
|
||||
setTaskIdentifier(issuedIdentifier, for: state)
|
||||
|
||||
if let block = progressBlock {
|
||||
options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
|
||||
}
|
||||
|
||||
let task = KingfisherManager.shared.retrieveImage(
|
||||
with: source,
|
||||
options: options,
|
||||
downloadTaskUpdated: { mutatingSelf.imageTask = $0 },
|
||||
progressiveImageSetter: { self.base.setImage($0, for: state) },
|
||||
referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier(for: state) },
|
||||
completionHandler: { result in
|
||||
CallbackQueue.mainCurrentOrAsync.execute {
|
||||
guard issuedIdentifier == self.taskIdentifier(for: state) else {
|
||||
let reason: KingfisherError.ImageSettingErrorReason
|
||||
do {
|
||||
let value = try result.get()
|
||||
reason = .notCurrentSourceTask(result: value, error: nil, source: source)
|
||||
} catch {
|
||||
reason = .notCurrentSourceTask(result: nil, error: error, source: source)
|
||||
}
|
||||
let error = KingfisherError.imageSettingError(reason: reason)
|
||||
completionHandler?(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
mutatingSelf.imageTask = nil
|
||||
mutatingSelf.setTaskIdentifier(nil, for: state)
|
||||
|
||||
switch result {
|
||||
case .success(let value):
|
||||
self.base.setImage(value.image, for: state)
|
||||
completionHandler?(result)
|
||||
|
||||
case .failure:
|
||||
if let image = options.onFailureImage {
|
||||
self.base.setImage(image, for: state)
|
||||
}
|
||||
completionHandler?(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
mutatingSelf.imageTask = task
|
||||
return task
|
||||
}
|
||||
|
||||
// MARK: Cancelling Downloading Task
|
||||
|
||||
/// Cancels the image download task of the button if it is running.
|
||||
/// Nothing will happen if the downloading has already finished.
|
||||
public func cancelImageDownloadTask() {
|
||||
imageTask?.cancel()
|
||||
}
|
||||
|
||||
// MARK: Setting Background Image
|
||||
|
||||
/// Sets a background image to the button for a specified state with a source.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - source: The `Source` object contains information about the image.
|
||||
/// - state: The button state to which the image should be set.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested source
|
||||
/// Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setBackgroundImage(
|
||||
with source: Source?,
|
||||
for state: UIControl.State,
|
||||
placeholder: UIImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
|
||||
return setBackgroundImage(
|
||||
with: source,
|
||||
for: state,
|
||||
placeholder: placeholder,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets a background image to the button for a specified state with a requested resource.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - resource: The `Resource` object contains information about the resource.
|
||||
/// - state: The button state to which the image should be set.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
|
||||
/// or network. Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setBackgroundImage(
|
||||
with resource: Resource?,
|
||||
for state: UIControl.State,
|
||||
placeholder: UIImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
return setBackgroundImage(
|
||||
with: resource?.convertToSource(),
|
||||
for: state,
|
||||
placeholder: placeholder,
|
||||
options: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func setBackgroundImage(
|
||||
with source: Source?,
|
||||
for state: UIControl.State,
|
||||
placeholder: UIImage? = nil,
|
||||
parsedOptions: KingfisherParsedOptionsInfo,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
guard let source = source else {
|
||||
base.setBackgroundImage(placeholder, for: state)
|
||||
setBackgroundTaskIdentifier(nil, for: state)
|
||||
completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
|
||||
return nil
|
||||
}
|
||||
|
||||
var options = parsedOptions
|
||||
if !options.keepCurrentImageWhileLoading {
|
||||
base.setBackgroundImage(placeholder, for: state)
|
||||
}
|
||||
|
||||
var mutatingSelf = self
|
||||
let issuedIdentifier = Source.Identifier.next()
|
||||
setBackgroundTaskIdentifier(issuedIdentifier, for: state)
|
||||
|
||||
if let block = progressBlock {
|
||||
options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
|
||||
}
|
||||
|
||||
let task = KingfisherManager.shared.retrieveImage(
|
||||
with: source,
|
||||
options: options,
|
||||
downloadTaskUpdated: { mutatingSelf.backgroundImageTask = $0 },
|
||||
progressiveImageSetter: { self.base.setBackgroundImage($0, for: state) },
|
||||
referenceTaskIdentifierChecker: { issuedIdentifier == self.backgroundTaskIdentifier(for: state) },
|
||||
completionHandler: { result in
|
||||
CallbackQueue.mainCurrentOrAsync.execute {
|
||||
guard issuedIdentifier == self.backgroundTaskIdentifier(for: state) else {
|
||||
let reason: KingfisherError.ImageSettingErrorReason
|
||||
do {
|
||||
let value = try result.get()
|
||||
reason = .notCurrentSourceTask(result: value, error: nil, source: source)
|
||||
} catch {
|
||||
reason = .notCurrentSourceTask(result: nil, error: error, source: source)
|
||||
}
|
||||
let error = KingfisherError.imageSettingError(reason: reason)
|
||||
completionHandler?(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
mutatingSelf.backgroundImageTask = nil
|
||||
mutatingSelf.setBackgroundTaskIdentifier(nil, for: state)
|
||||
|
||||
switch result {
|
||||
case .success(let value):
|
||||
self.base.setBackgroundImage(value.image, for: state)
|
||||
completionHandler?(result)
|
||||
|
||||
case .failure:
|
||||
if let image = options.onFailureImage {
|
||||
self.base.setBackgroundImage(image, for: state)
|
||||
}
|
||||
completionHandler?(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
mutatingSelf.backgroundImageTask = task
|
||||
return task
|
||||
}
|
||||
|
||||
// MARK: Cancelling Background Downloading Task
|
||||
|
||||
/// Cancels the background image download task of the button if it is running.
|
||||
/// Nothing will happen if the downloading has already finished.
|
||||
public func cancelBackgroundImageDownloadTask() {
|
||||
backgroundImageTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Associated Object
|
||||
private var taskIdentifierKey: Void?
|
||||
private var imageTaskKey: Void?
|
||||
|
||||
// MARK: Properties
|
||||
extension KingfisherWrapper where Base: UIButton {
|
||||
|
||||
private typealias TaskIdentifier = Box<[UInt: Source.Identifier.Value]>
|
||||
|
||||
public func taskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? {
|
||||
return taskIdentifierInfo.value[state.rawValue]
|
||||
}
|
||||
|
||||
private func setTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) {
|
||||
taskIdentifierInfo.value[state.rawValue] = identifier
|
||||
}
|
||||
|
||||
private var taskIdentifierInfo: TaskIdentifier {
|
||||
return getAssociatedObject(base, &taskIdentifierKey) ?? {
|
||||
setRetainedAssociatedObject(base, &taskIdentifierKey, $0)
|
||||
return $0
|
||||
} (TaskIdentifier([:]))
|
||||
}
|
||||
|
||||
private var imageTask: DownloadTask? {
|
||||
get { return getAssociatedObject(base, &imageTaskKey) }
|
||||
set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var backgroundTaskIdentifierKey: Void?
|
||||
private var backgroundImageTaskKey: Void?
|
||||
|
||||
// MARK: Background Properties
|
||||
extension KingfisherWrapper where Base: UIButton {
|
||||
|
||||
public func backgroundTaskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? {
|
||||
return backgroundTaskIdentifierInfo.value[state.rawValue]
|
||||
}
|
||||
|
||||
private func setBackgroundTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) {
|
||||
backgroundTaskIdentifierInfo.value[state.rawValue] = identifier
|
||||
}
|
||||
|
||||
private var backgroundTaskIdentifierInfo: TaskIdentifier {
|
||||
return getAssociatedObject(base, &backgroundTaskIdentifierKey) ?? {
|
||||
setRetainedAssociatedObject(base, &backgroundTaskIdentifierKey, $0)
|
||||
return $0
|
||||
} (TaskIdentifier([:]))
|
||||
}
|
||||
|
||||
private var backgroundImageTask: DownloadTask? {
|
||||
get { return getAssociatedObject(base, &backgroundImageTaskKey) }
|
||||
mutating set { setRetainedAssociatedObject(base, &backgroundImageTaskKey, newValue) }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
204
Pods/Kingfisher/Sources/Extensions/WKInterfaceImage+Kingfisher.swift
generated
Normal file
204
Pods/Kingfisher/Sources/Extensions/WKInterfaceImage+Kingfisher.swift
generated
Normal file
@@ -0,0 +1,204 @@
|
||||
//
|
||||
// WKInterfaceImage+Kingfisher.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Rodrigo Borges Soares on 04/05/18.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#if canImport(WatchKit)
|
||||
|
||||
import WatchKit
|
||||
|
||||
extension KingfisherWrapper where Base: WKInterfaceImage {
|
||||
|
||||
// MARK: Setting Image
|
||||
|
||||
/// Sets an image to the image view with a source.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - source: The `Source` object contains information about the image.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
///
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested source
|
||||
/// Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with source: Source?,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
|
||||
return setImage(
|
||||
with: source,
|
||||
placeholder: placeholder,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets an image to the image view with a requested resource.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - resource: The `Resource` object contains information about the image.
|
||||
/// - placeholder: A placeholder to show while retrieving the image from the given `resource`.
|
||||
/// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called.
|
||||
/// - completionHandler: Called when the image retrieved and set finished.
|
||||
/// - Returns: A task represents the image downloading.
|
||||
///
|
||||
/// - Note:
|
||||
///
|
||||
/// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache
|
||||
/// or network. Since this method will perform UI changes, you must call it from the main thread.
|
||||
/// Both `progressBlock` and `completionHandler` will be also executed in the main thread.
|
||||
///
|
||||
@discardableResult
|
||||
public func setImage(
|
||||
with resource: Resource?,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
return setImage(
|
||||
with: resource?.convertToSource(),
|
||||
placeholder: placeholder,
|
||||
options: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func setImage(
|
||||
with source: Source?,
|
||||
placeholder: KFCrossPlatformImage? = nil,
|
||||
parsedOptions: KingfisherParsedOptionsInfo,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
var mutatingSelf = self
|
||||
guard let source = source else {
|
||||
base.setImage(placeholder)
|
||||
mutatingSelf.taskIdentifier = nil
|
||||
completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
|
||||
return nil
|
||||
}
|
||||
|
||||
var options = parsedOptions
|
||||
if !options.keepCurrentImageWhileLoading {
|
||||
base.setImage(placeholder)
|
||||
}
|
||||
|
||||
let issuedIdentifier = Source.Identifier.next()
|
||||
mutatingSelf.taskIdentifier = issuedIdentifier
|
||||
|
||||
if let block = progressBlock {
|
||||
options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
|
||||
}
|
||||
|
||||
let task = KingfisherManager.shared.retrieveImage(
|
||||
with: source,
|
||||
options: options,
|
||||
downloadTaskUpdated: { mutatingSelf.imageTask = $0 },
|
||||
progressiveImageSetter: { self.base.setImage($0) },
|
||||
referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier },
|
||||
completionHandler: { result in
|
||||
CallbackQueue.mainCurrentOrAsync.execute {
|
||||
guard issuedIdentifier == self.taskIdentifier else {
|
||||
let reason: KingfisherError.ImageSettingErrorReason
|
||||
do {
|
||||
let value = try result.get()
|
||||
reason = .notCurrentSourceTask(result: value, error: nil, source: source)
|
||||
} catch {
|
||||
reason = .notCurrentSourceTask(result: nil, error: error, source: source)
|
||||
}
|
||||
let error = KingfisherError.imageSettingError(reason: reason)
|
||||
completionHandler?(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
mutatingSelf.imageTask = nil
|
||||
mutatingSelf.taskIdentifier = nil
|
||||
|
||||
switch result {
|
||||
case .success(let value):
|
||||
self.base.setImage(value.image)
|
||||
completionHandler?(result)
|
||||
|
||||
case .failure:
|
||||
if let image = options.onFailureImage {
|
||||
self.base.setImage(image)
|
||||
}
|
||||
completionHandler?(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
mutatingSelf.imageTask = task
|
||||
return task
|
||||
}
|
||||
|
||||
// MARK: Cancelling Image
|
||||
|
||||
/// Cancel the image download task bounded to the image view if it is running.
|
||||
/// Nothing will happen if the downloading has already finished.
|
||||
public func cancelDownloadTask() {
|
||||
imageTask?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private var taskIdentifierKey: Void?
|
||||
private var imageTaskKey: Void?
|
||||
|
||||
// MARK: Properties
|
||||
extension KingfisherWrapper where Base: WKInterfaceImage {
|
||||
|
||||
public private(set) var taskIdentifier: Source.Identifier.Value? {
|
||||
get {
|
||||
let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)
|
||||
return box?.value
|
||||
}
|
||||
set {
|
||||
let box = newValue.map { Box($0) }
|
||||
setRetainedAssociatedObject(base, &taskIdentifierKey, box)
|
||||
}
|
||||
}
|
||||
|
||||
private var imageTask: DownloadTask? {
|
||||
get { return getAssociatedObject(base, &imageTaskKey) }
|
||||
set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
148
Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift
generated
Normal file
148
Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift
generated
Normal file
@@ -0,0 +1,148 @@
|
||||
//
|
||||
// AVAssetImageDataProvider.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2020/08/09.
|
||||
//
|
||||
// Copyright (c) 2020 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.
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
import Foundation
|
||||
import AVKit
|
||||
|
||||
#if canImport(MobileCoreServices)
|
||||
import MobileCoreServices
|
||||
#else
|
||||
import CoreServices
|
||||
#endif
|
||||
|
||||
/// A data provider to provide thumbnail data from a given AVKit asset.
|
||||
public struct AVAssetImageDataProvider: ImageDataProvider {
|
||||
|
||||
/// The possible error might be caused by the `AVAssetImageDataProvider`.
|
||||
/// - userCancelled: The data provider process is cancelled.
|
||||
/// - invalidImage: The retrieved image is invalid.
|
||||
public enum AVAssetImageDataProviderError: Error {
|
||||
case userCancelled
|
||||
case invalidImage(_ image: CGImage?)
|
||||
}
|
||||
|
||||
/// The asset image generator bound to `self`.
|
||||
public let assetImageGenerator: AVAssetImageGenerator
|
||||
|
||||
/// The time at which the image should be generate in the asset.
|
||||
public let time: CMTime
|
||||
|
||||
private var internalKey: String {
|
||||
return (assetImageGenerator.asset as? AVURLAsset)?.url.absoluteString ?? UUID().uuidString
|
||||
}
|
||||
|
||||
/// The cache key used by `self`.
|
||||
public var cacheKey: String {
|
||||
return "\(internalKey)_\(time.seconds)"
|
||||
}
|
||||
|
||||
/// Creates an asset image data provider.
|
||||
/// - Parameters:
|
||||
/// - assetImageGenerator: The asset image generator controls data providing behaviors.
|
||||
/// - time: At which time in the asset the image should be generated.
|
||||
public init(assetImageGenerator: AVAssetImageGenerator, time: CMTime) {
|
||||
self.assetImageGenerator = assetImageGenerator
|
||||
self.time = time
|
||||
}
|
||||
|
||||
/// Creates an asset image data provider.
|
||||
/// - Parameters:
|
||||
/// - assetURL: The URL of asset for providing image data.
|
||||
/// - time: At which time in the asset the image should be generated.
|
||||
///
|
||||
/// This method uses `assetURL` to create an `AVAssetImageGenerator` object and calls
|
||||
/// the `init(assetImageGenerator:time:)` initializer.
|
||||
///
|
||||
public init(assetURL: URL, time: CMTime) {
|
||||
let asset = AVAsset(url: assetURL)
|
||||
let generator = AVAssetImageGenerator(asset: asset)
|
||||
generator.appliesPreferredTrackTransform = true
|
||||
self.init(assetImageGenerator: generator, time: time)
|
||||
}
|
||||
|
||||
/// Creates an asset image data provider.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - assetURL: The URL of asset for providing image data.
|
||||
/// - seconds: At which time in seconds in the asset the image should be generated.
|
||||
///
|
||||
/// This method uses `assetURL` to create an `AVAssetImageGenerator` object, uses `seconds` to create a `CMTime`,
|
||||
/// and calls the `init(assetImageGenerator:time:)` initializer.
|
||||
///
|
||||
public init(assetURL: URL, seconds: TimeInterval) {
|
||||
let time = CMTime(seconds: seconds, preferredTimescale: 600)
|
||||
self.init(assetURL: assetURL, time: time)
|
||||
}
|
||||
|
||||
public func data(handler: @escaping (Result<Data, Error>) -> Void) {
|
||||
assetImageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: time)]) {
|
||||
(requestedTime, image, imageTime, result, error) in
|
||||
if let error = error {
|
||||
handler(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
if result == .cancelled {
|
||||
handler(.failure(AVAssetImageDataProviderError.userCancelled))
|
||||
return
|
||||
}
|
||||
|
||||
guard let cgImage = image, let data = cgImage.jpegData else {
|
||||
handler(.failure(AVAssetImageDataProviderError.invalidImage(image)))
|
||||
return
|
||||
}
|
||||
|
||||
handler(.success(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CGImage {
|
||||
var jpegData: Data? {
|
||||
guard let mutableData = CFDataCreateMutable(nil, 0) else {
|
||||
return nil
|
||||
}
|
||||
#if os(xrOS)
|
||||
guard let destination = CGImageDestinationCreateWithData(
|
||||
mutableData, UTType.jpeg.identifier as CFString , 1, nil
|
||||
) else {
|
||||
return nil
|
||||
}
|
||||
#else
|
||||
guard let destination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) else {
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
|
||||
CGImageDestinationAddImage(destination, self, nil)
|
||||
guard CGImageDestinationFinalize(destination) else { return nil }
|
||||
return mutableData as Data
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
190
Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift
generated
Normal file
190
Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift
generated
Normal file
@@ -0,0 +1,190 @@
|
||||
//
|
||||
// ImageDataProvider.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2018/11/13.
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Represents a data provider to provide image data to Kingfisher when setting with
|
||||
/// `Source.provider` source. Compared to `Source.network` member, it gives a chance
|
||||
/// to load some image data in your own way, as long as you can provide the data
|
||||
/// representation for the image.
|
||||
public protocol ImageDataProvider {
|
||||
|
||||
/// The key used in cache.
|
||||
var cacheKey: String { get }
|
||||
|
||||
/// Provides the data which represents image. Kingfisher uses the data you pass in the
|
||||
/// handler to process images and caches it for later use.
|
||||
///
|
||||
/// - Parameter handler: The handler you should call when you prepared your data.
|
||||
/// If the data is loaded successfully, call the handler with
|
||||
/// a `.success` with the data associated. Otherwise, call it
|
||||
/// with a `.failure` and pass the error.
|
||||
///
|
||||
/// - Note:
|
||||
/// If the `handler` is called with a `.failure` with error, a `dataProviderError` of
|
||||
/// `ImageSettingErrorReason` will be finally thrown out to you as the `KingfisherError`
|
||||
/// from the framework.
|
||||
func data(handler: @escaping (Result<Data, Error>) -> Void)
|
||||
|
||||
/// The content URL represents this provider, if exists.
|
||||
var contentURL: URL? { get }
|
||||
}
|
||||
|
||||
public extension ImageDataProvider {
|
||||
var contentURL: URL? { return nil }
|
||||
func convertToSource() -> Source {
|
||||
.provider(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an image data provider for loading from a local file URL on disk.
|
||||
/// Uses this type for adding a disk image to Kingfisher. Compared to loading it
|
||||
/// directly, you can get benefit of using Kingfisher's extension methods, as well
|
||||
/// as applying `ImageProcessor`s and storing the image to `ImageCache` of Kingfisher.
|
||||
public struct LocalFileImageDataProvider: ImageDataProvider {
|
||||
|
||||
// MARK: Public Properties
|
||||
|
||||
/// The file URL from which the image be loaded.
|
||||
public let fileURL: URL
|
||||
private let loadingQueue: ExecutionQueue
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates an image data provider by supplying the target local file URL.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - fileURL: The file URL from which the image be loaded.
|
||||
/// - cacheKey: The key is used for caching the image data. By default,
|
||||
/// the `absoluteString` of `fileURL` is used.
|
||||
/// - loadingQueue: The queue where the file loading should happen. By default, the dispatch queue of
|
||||
/// `.global(qos: .userInitiated)` will be used.
|
||||
public init(
|
||||
fileURL: URL,
|
||||
cacheKey: String? = nil,
|
||||
loadingQueue: ExecutionQueue = .dispatch(DispatchQueue.global(qos: .userInitiated))
|
||||
) {
|
||||
self.fileURL = fileURL
|
||||
self.cacheKey = cacheKey ?? fileURL.localFileCacheKey
|
||||
self.loadingQueue = loadingQueue
|
||||
}
|
||||
|
||||
// MARK: Protocol Conforming
|
||||
|
||||
/// The key used in cache.
|
||||
public var cacheKey: String
|
||||
|
||||
public func data(handler:@escaping (Result<Data, Error>) -> Void) {
|
||||
loadingQueue.execute {
|
||||
handler(Result(catching: { try Data(contentsOf: fileURL) }))
|
||||
}
|
||||
}
|
||||
|
||||
#if swift(>=5.5)
|
||||
#if canImport(_Concurrency)
|
||||
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
|
||||
public var data: Data {
|
||||
get async throws {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
loadingQueue.execute {
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL)
|
||||
continuation.resume(returning: data)
|
||||
} catch {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/// The URL of the local file on the disk.
|
||||
public var contentURL: URL? {
|
||||
return fileURL
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an image data provider for loading image from a given Base64 encoded string.
|
||||
public struct Base64ImageDataProvider: ImageDataProvider {
|
||||
|
||||
// MARK: Public Properties
|
||||
/// The encoded Base64 string for the image.
|
||||
public let base64String: String
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates an image data provider by supplying the Base64 encoded string.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - base64String: The Base64 encoded string for an image.
|
||||
/// - cacheKey: The key is used for caching the image data. You need a different key for any different image.
|
||||
public init(base64String: String, cacheKey: String) {
|
||||
self.base64String = base64String
|
||||
self.cacheKey = cacheKey
|
||||
}
|
||||
|
||||
// MARK: Protocol Conforming
|
||||
|
||||
/// The key used in cache.
|
||||
public var cacheKey: String
|
||||
|
||||
public func data(handler: (Result<Data, Error>) -> Void) {
|
||||
let data = Data(base64Encoded: base64String)!
|
||||
handler(.success(data))
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an image data provider for a raw data object.
|
||||
public struct RawImageDataProvider: ImageDataProvider {
|
||||
|
||||
// MARK: Public Properties
|
||||
|
||||
/// The raw data object to provide to Kingfisher image loader.
|
||||
public let data: Data
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates an image data provider by the given raw `data` value and a `cacheKey` be used in Kingfisher cache.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - data: The raw data reprensents an image.
|
||||
/// - cacheKey: The key is used for caching the image data. You need a different key for any different image.
|
||||
public init(data: Data, cacheKey: String) {
|
||||
self.data = data
|
||||
self.cacheKey = cacheKey
|
||||
}
|
||||
|
||||
// MARK: Protocol Conforming
|
||||
|
||||
/// The key used in cache.
|
||||
public var cacheKey: String
|
||||
|
||||
public func data(handler: @escaping (Result<Data, Error>) -> Void) {
|
||||
handler(.success(data))
|
||||
}
|
||||
}
|
||||
121
Pods/Kingfisher/Sources/General/ImageSource/Resource.swift
generated
Normal file
121
Pods/Kingfisher/Sources/General/ImageSource/Resource.swift
generated
Normal file
@@ -0,0 +1,121 @@
|
||||
//
|
||||
// Resource.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
|
||||
|
||||
/// Represents an image resource at a certain url and a given cache key.
|
||||
/// Kingfisher will use a `Resource` to download a resource from network and cache it with the cache key when
|
||||
/// using `Source.network` as its image setting source.
|
||||
public protocol Resource {
|
||||
|
||||
/// The key used in cache.
|
||||
var cacheKey: String { get }
|
||||
|
||||
/// The target image URL.
|
||||
var downloadURL: URL { get }
|
||||
}
|
||||
|
||||
extension Resource {
|
||||
|
||||
/// Converts `self` to a valid `Source` based on its `downloadURL` scheme. A `.provider` with
|
||||
/// `LocalFileImageDataProvider` associated will be returned if the URL points to a local file. Otherwise,
|
||||
/// `.network` is returned.
|
||||
public func convertToSource(overrideCacheKey: String? = nil) -> Source {
|
||||
let key = overrideCacheKey ?? cacheKey
|
||||
return downloadURL.isFileURL ?
|
||||
.provider(LocalFileImageDataProvider(fileURL: downloadURL, cacheKey: key)) :
|
||||
.network(KF.ImageResource(downloadURL: downloadURL, cacheKey: key))
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, deprecated, message: "This type conflicts with `GeneratedAssetSymbols.ImageResource` in Swift 5.9. Renamed to avoid issues in the future.", renamed: "KF.ImageResource")
|
||||
public typealias ImageResource = KF.ImageResource
|
||||
|
||||
|
||||
extension KF {
|
||||
/// ImageResource is a simple combination of `downloadURL` and `cacheKey`.
|
||||
/// When passed to image view set methods, Kingfisher will try to download the target
|
||||
/// image from the `downloadURL`, and then store it with the `cacheKey` as the key in cache.
|
||||
public struct ImageResource: Resource {
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
/// Creates an image resource.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - downloadURL: The target image URL from where the image can be downloaded.
|
||||
/// - cacheKey: The cache key. If `nil`, Kingfisher will use the `absoluteString` of `downloadURL` as the key.
|
||||
/// Default is `nil`.
|
||||
public init(downloadURL: URL, cacheKey: String? = nil) {
|
||||
self.downloadURL = downloadURL
|
||||
self.cacheKey = cacheKey ?? downloadURL.cacheKey
|
||||
}
|
||||
|
||||
// MARK: Protocol Conforming
|
||||
|
||||
/// The key used in cache.
|
||||
public let cacheKey: String
|
||||
|
||||
/// The target image URL.
|
||||
public let downloadURL: URL
|
||||
}
|
||||
}
|
||||
|
||||
/// URL conforms to `Resource` in Kingfisher.
|
||||
/// The `absoluteString` of this URL is used as `cacheKey`. And the URL itself will be used as `downloadURL`.
|
||||
/// If you need customize the url and/or cache key, use `ImageResource` instead.
|
||||
extension URL: Resource {
|
||||
public var cacheKey: String { return isFileURL ? localFileCacheKey : absoluteString }
|
||||
public var downloadURL: URL { return self }
|
||||
}
|
||||
|
||||
extension URL {
|
||||
static let localFileCacheKeyPrefix = "kingfisher.local.cacheKey"
|
||||
|
||||
// The special version of cache key for a local file on disk. Every time the app is reinstalled on the disk,
|
||||
// the system assigns a new container folder to hold the .app (and the extensions, .appex) folder. So the URL for
|
||||
// the same image in bundle might be different.
|
||||
//
|
||||
// This getter only uses the fixed part in the URL (until the bundle name folder) to provide a stable cache key
|
||||
// for the image under the same path inside the bundle.
|
||||
//
|
||||
// See #1825 (https://github.com/onevcat/Kingfisher/issues/1825)
|
||||
var localFileCacheKey: String {
|
||||
var validComponents: [String] = []
|
||||
for part in pathComponents.reversed() {
|
||||
validComponents.append(part)
|
||||
if part.hasSuffix(".app") || part.hasSuffix(".appex") {
|
||||
break
|
||||
}
|
||||
}
|
||||
let fixedPath = "\(Self.localFileCacheKeyPrefix)/\(validComponents.reversed().joined(separator: "/"))"
|
||||
if let q = query {
|
||||
return "\(fixedPath)?\(q)"
|
||||
} else {
|
||||
return fixedPath
|
||||
}
|
||||
}
|
||||
}
|
||||
116
Pods/Kingfisher/Sources/General/ImageSource/Source.swift
generated
Normal file
116
Pods/Kingfisher/Sources/General/ImageSource/Source.swift
generated
Normal file
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// Source.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2018/11/17.
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Represents an image setting source for Kingfisher methods.
|
||||
///
|
||||
/// A `Source` value indicates the way how the target image can be retrieved and cached.
|
||||
///
|
||||
/// - network: The target image should be got from network remotely. The associated `Resource`
|
||||
/// value defines detail information like image URL and cache key.
|
||||
/// - provider: The target image should be provided in a data format. Normally, it can be an image
|
||||
/// from local storage or in any other encoding format (like Base64).
|
||||
public enum Source {
|
||||
|
||||
/// Represents the source task identifier when setting an image to a view with extension methods.
|
||||
public enum Identifier {
|
||||
|
||||
/// The underlying value type of source identifier.
|
||||
public typealias Value = UInt
|
||||
static private(set) var current: Value = 0
|
||||
static func next() -> Value {
|
||||
current += 1
|
||||
return current
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Member Cases
|
||||
|
||||
/// The target image should be got from network remotely. The associated `Resource`
|
||||
/// value defines detail information like image URL and cache key.
|
||||
case network(Resource)
|
||||
|
||||
/// The target image should be provided in a data format. Normally, it can be an image
|
||||
/// from local storage or in any other encoding format (like Base64).
|
||||
case provider(ImageDataProvider)
|
||||
|
||||
// MARK: Getting Properties
|
||||
|
||||
/// The cache key defined for this source value.
|
||||
public var cacheKey: String {
|
||||
switch self {
|
||||
case .network(let resource): return resource.cacheKey
|
||||
case .provider(let provider): return provider.cacheKey
|
||||
}
|
||||
}
|
||||
|
||||
/// The URL defined for this source value.
|
||||
///
|
||||
/// For a `.network` source, it is the `downloadURL` of associated `Resource` instance.
|
||||
/// For a `.provider` value, it is always `nil`.
|
||||
public var url: URL? {
|
||||
switch self {
|
||||
case .network(let resource): return resource.downloadURL
|
||||
case .provider(let provider): return provider.contentURL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Source: Hashable {
|
||||
public static func == (lhs: Source, rhs: Source) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.network(let r1), .network(let r2)):
|
||||
return r1.cacheKey == r2.cacheKey && r1.downloadURL == r2.downloadURL
|
||||
case (.provider(let p1), .provider(let p2)):
|
||||
return p1.cacheKey == p2.cacheKey && p1.contentURL == p2.contentURL
|
||||
case (.provider(_), .network(_)):
|
||||
return false
|
||||
case (.network(_), .provider(_)):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .network(let r):
|
||||
hasher.combine(r.cacheKey)
|
||||
hasher.combine(r.downloadURL)
|
||||
case .provider(let p):
|
||||
hasher.combine(p.cacheKey)
|
||||
hasher.combine(p.contentURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Source {
|
||||
var asResource: Resource? {
|
||||
guard case .network(let resource) = self else {
|
||||
return nil
|
||||
}
|
||||
return resource
|
||||
}
|
||||
}
|
||||
442
Pods/Kingfisher/Sources/General/KF.swift
generated
Normal file
442
Pods/Kingfisher/Sources/General/KF.swift
generated
Normal file
@@ -0,0 +1,442 @@
|
||||
//
|
||||
// KF.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2020/09/21.
|
||||
//
|
||||
// Copyright (c) 2020 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.
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
#if canImport(CarPlay) && !targetEnvironment(macCatalyst)
|
||||
import CarPlay
|
||||
#endif
|
||||
|
||||
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
#if canImport(WatchKit)
|
||||
import WatchKit
|
||||
#endif
|
||||
|
||||
#if canImport(TVUIKit)
|
||||
import TVUIKit
|
||||
#endif
|
||||
|
||||
/// A helper type to create image setting tasks in a builder pattern.
|
||||
/// Use methods in this type to create a `KF.Builder` instance and configure image tasks there.
|
||||
public enum KF {
|
||||
|
||||
/// Creates a builder for a given `Source`.
|
||||
/// - Parameter source: The `Source` object defines data information from network or a data provider.
|
||||
/// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)`
|
||||
/// to start the image loading.
|
||||
public static func source(_ source: Source?) -> KF.Builder {
|
||||
Builder(source: source)
|
||||
}
|
||||
|
||||
/// Creates a builder for a given `Resource`.
|
||||
/// - Parameter resource: The `Resource` object defines data information like key or URL.
|
||||
/// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)`
|
||||
/// to start the image loading.
|
||||
public static func resource(_ resource: Resource?) -> KF.Builder {
|
||||
source(resource?.convertToSource())
|
||||
}
|
||||
|
||||
/// Creates a builder for a given `URL` and an optional cache key.
|
||||
/// - Parameters:
|
||||
/// - url: The URL where the image should be downloaded.
|
||||
/// - cacheKey: The key used to store the downloaded image in cache.
|
||||
/// If `nil`, the `absoluteString` of `url` is used as the cache key.
|
||||
/// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)`
|
||||
/// to start the image loading.
|
||||
public static func url(_ url: URL?, cacheKey: String? = nil) -> KF.Builder {
|
||||
source(url?.convertToSource(overrideCacheKey: cacheKey))
|
||||
}
|
||||
|
||||
/// Creates a builder for a given `ImageDataProvider`.
|
||||
/// - Parameter provider: The `ImageDataProvider` object contains information about the data.
|
||||
/// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)`
|
||||
/// to start the image loading.
|
||||
public static func dataProvider(_ provider: ImageDataProvider?) -> KF.Builder {
|
||||
source(provider?.convertToSource())
|
||||
}
|
||||
|
||||
/// Creates a builder for some given raw data and a cache key.
|
||||
/// - Parameters:
|
||||
/// - data: The data object from which the image should be created.
|
||||
/// - cacheKey: The key used to store the downloaded image in cache.
|
||||
/// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)`
|
||||
/// to start the image loading.
|
||||
public static func data(_ data: Data?, cacheKey: String) -> KF.Builder {
|
||||
if let data = data {
|
||||
return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey))
|
||||
} else {
|
||||
return dataProvider(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension KF {
|
||||
|
||||
/// A builder class to configure an image retrieving task and set it to a holder view or component.
|
||||
public class Builder {
|
||||
private let source: Source?
|
||||
|
||||
#if os(watchOS)
|
||||
private var placeholder: KFCrossPlatformImage?
|
||||
#else
|
||||
private var placeholder: Placeholder?
|
||||
#endif
|
||||
|
||||
public var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions)
|
||||
|
||||
public let onFailureDelegate = Delegate<KingfisherError, Void>()
|
||||
public let onSuccessDelegate = Delegate<RetrieveImageResult, Void>()
|
||||
public let onProgressDelegate = Delegate<(Int64, Int64), Void>()
|
||||
|
||||
init(source: Source?) {
|
||||
self.source = source
|
||||
}
|
||||
|
||||
private var resultHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? {
|
||||
{
|
||||
switch $0 {
|
||||
case .success(let result):
|
||||
self.onSuccessDelegate(result)
|
||||
case .failure(let error):
|
||||
self.onFailureDelegate(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var progressBlock: DownloadProgressBlock {
|
||||
{ self.onProgressDelegate(($0, $1)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension KF.Builder {
|
||||
#if !os(watchOS)
|
||||
|
||||
/// Builds the image task request and sets it to an image view.
|
||||
/// - Parameter imageView: The image view which loads the task and should be set with the image.
|
||||
/// - Returns: A task represents the image downloading, if initialized.
|
||||
/// This value is `nil` if the image is being loaded from cache.
|
||||
@discardableResult
|
||||
public func set(to imageView: KFCrossPlatformImageView) -> DownloadTask? {
|
||||
imageView.kf.setImage(
|
||||
with: source,
|
||||
placeholder: placeholder,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: resultHandler
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds the image task request and sets it to an `NSTextAttachment` object.
|
||||
/// - Parameters:
|
||||
/// - attachment: The text attachment object which loads the task and should be set with the image.
|
||||
/// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added.
|
||||
/// - Returns: A task represents the image downloading, if initialized.
|
||||
/// This value is `nil` if the image is being loaded from cache.
|
||||
@discardableResult
|
||||
public func set(to attachment: NSTextAttachment, attributedView: @autoclosure @escaping () -> KFCrossPlatformView) -> DownloadTask? {
|
||||
let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil
|
||||
return attachment.kf.setImage(
|
||||
with: source,
|
||||
attributedView: attributedView,
|
||||
placeholder: placeholderImage,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: resultHandler
|
||||
)
|
||||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
|
||||
/// Builds the image task request and sets it to a button.
|
||||
/// - Parameters:
|
||||
/// - button: The button which loads the task and should be set with the image.
|
||||
/// - state: The button state to which the image should be set.
|
||||
/// - Returns: A task represents the image downloading, if initialized.
|
||||
/// This value is `nil` if the image is being loaded from cache.
|
||||
@discardableResult
|
||||
public func set(to button: UIButton, for state: UIControl.State) -> DownloadTask? {
|
||||
let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil
|
||||
return button.kf.setImage(
|
||||
with: source,
|
||||
for: state,
|
||||
placeholder: placeholderImage,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: resultHandler
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds the image task request and sets it to the background image for a button.
|
||||
/// - Parameters:
|
||||
/// - button: The button which loads the task and should be set with the image.
|
||||
/// - state: The button state to which the image should be set.
|
||||
/// - Returns: A task represents the image downloading, if initialized.
|
||||
/// This value is `nil` if the image is being loaded from cache.
|
||||
@discardableResult
|
||||
public func setBackground(to button: UIButton, for state: UIControl.State) -> DownloadTask? {
|
||||
let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil
|
||||
return button.kf.setBackgroundImage(
|
||||
with: source,
|
||||
for: state,
|
||||
placeholder: placeholderImage,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: resultHandler
|
||||
)
|
||||
}
|
||||
#endif // end of canImport(UIKit)
|
||||
|
||||
#if canImport(CarPlay) && !targetEnvironment(macCatalyst)
|
||||
|
||||
/// Builds the image task request and sets it to the image for a list item.
|
||||
/// - Parameters:
|
||||
/// - listItem: The list item which loads the task and should be set with the image.
|
||||
/// - Returns: A task represents the image downloading, if initialized.
|
||||
/// This value is `nil` if the image is being loaded from cache.
|
||||
@available(iOS 14.0, *)
|
||||
@discardableResult
|
||||
public func set(to listItem: CPListItem) -> DownloadTask? {
|
||||
let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil
|
||||
return listItem.kf.setImage(
|
||||
with: source,
|
||||
placeholder: placeholderImage,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: resultHandler
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
|
||||
/// Builds the image task request and sets it to a button.
|
||||
/// - Parameter button: The button which loads the task and should be set with the image.
|
||||
/// - Returns: A task represents the image downloading, if initialized.
|
||||
/// This value is `nil` if the image is being loaded from cache.
|
||||
@discardableResult
|
||||
public func set(to button: NSButton) -> DownloadTask? {
|
||||
let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil
|
||||
return button.kf.setImage(
|
||||
with: source,
|
||||
placeholder: placeholderImage,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: resultHandler
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds the image task request and sets it to the alternative image for a button.
|
||||
/// - Parameter button: The button which loads the task and should be set with the image.
|
||||
/// - Returns: A task represents the image downloading, if initialized.
|
||||
/// This value is `nil` if the image is being loaded from cache.
|
||||
@discardableResult
|
||||
public func setAlternative(to button: NSButton) -> DownloadTask? {
|
||||
let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil
|
||||
return button.kf.setAlternateImage(
|
||||
with: source,
|
||||
placeholder: placeholderImage,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: resultHandler
|
||||
)
|
||||
}
|
||||
#endif // end of canImport(AppKit)
|
||||
#endif // end of !os(watchOS)
|
||||
|
||||
#if canImport(WatchKit)
|
||||
/// Builds the image task request and sets it to a `WKInterfaceImage` object.
|
||||
/// - Parameter interfaceImage: The watch interface image which loads the task and should be set with the image.
|
||||
/// - Returns: A task represents the image downloading, if initialized.
|
||||
/// This value is `nil` if the image is being loaded from cache.
|
||||
@discardableResult
|
||||
public func set(to interfaceImage: WKInterfaceImage) -> DownloadTask? {
|
||||
return interfaceImage.kf.setImage(
|
||||
with: source,
|
||||
placeholder: placeholder,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: resultHandler
|
||||
)
|
||||
}
|
||||
#endif // end of canImport(WatchKit)
|
||||
|
||||
#if canImport(TVUIKit)
|
||||
/// Builds the image task request and sets it to a TV monogram view.
|
||||
/// - Parameter monogramView: The monogram view which loads the task and should be set with the image.
|
||||
/// - Returns: A task represents the image downloading, if initialized.
|
||||
/// This value is `nil` if the image is being loaded from cache.
|
||||
@available(tvOS 12.0, *)
|
||||
@discardableResult
|
||||
public func set(to monogramView: TVMonogramView) -> DownloadTask? {
|
||||
let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil
|
||||
return monogramView.kf.setImage(
|
||||
with: source,
|
||||
placeholder: placeholderImage,
|
||||
parsedOptions: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: resultHandler
|
||||
)
|
||||
}
|
||||
#endif // end of canImport(TVUIKit)
|
||||
}
|
||||
|
||||
#if !os(watchOS)
|
||||
extension KF.Builder {
|
||||
#if os(iOS) || os(tvOS)
|
||||
|
||||
/// Sets a placeholder which is used while retrieving the image.
|
||||
/// - Parameter placeholder: A placeholder to show while retrieving the image from its source.
|
||||
/// - Returns: A `KF.Builder` with changes applied.
|
||||
public func placeholder(_ placeholder: Placeholder?) -> Self {
|
||||
self.placeholder = placeholder
|
||||
return self
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Sets a placeholder image which is used while retrieving the image.
|
||||
/// - Parameter placeholder: An image to show while retrieving the image from its source.
|
||||
/// - Returns: A `KF.Builder` with changes applied.
|
||||
public func placeholder(_ image: KFCrossPlatformImage?) -> Self {
|
||||
self.placeholder = image
|
||||
return self
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
extension KF.Builder {
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
/// Sets the transition for the image task.
|
||||
/// - Parameter transition: The desired transition effect when setting the image to image view.
|
||||
/// - Returns: A `KF.Builder` with changes applied.
|
||||
///
|
||||
/// Kingfisher will use the `transition` to animate the image in if it is downloaded from web.
|
||||
/// The transition will not happen when the
|
||||
/// image is retrieved from either memory or disk cache by default. If you need to do the transition even when
|
||||
/// the image being retrieved from cache, also call `forceRefresh()` on the returned `KF.Builder`.
|
||||
public func transition(_ transition: ImageTransition) -> Self {
|
||||
options.transition = transition
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets a fade transition for the image task.
|
||||
/// - Parameter duration: The duration of the fade transition.
|
||||
/// - Returns: A `KF.Builder` with changes applied.
|
||||
///
|
||||
/// Kingfisher will use the fade transition to animate the image in if it is downloaded from web.
|
||||
/// The transition will not happen when the
|
||||
/// image is retrieved from either memory or disk cache by default. If you need to do the transition even when
|
||||
/// the image being retrieved from cache, also call `forceRefresh()` on the returned `KF.Builder`.
|
||||
public func fade(duration: TimeInterval) -> Self {
|
||||
options.transition = .fade(duration)
|
||||
return self
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Sets whether keeping the existing image of image view while setting another image to it.
|
||||
/// - Parameter enabled: Whether the existing image should be kept.
|
||||
/// - Returns: A `KF.Builder` with changes applied.
|
||||
///
|
||||
/// By setting this option, the placeholder image parameter of image view extension method
|
||||
/// will be ignored and the current image will be kept while loading or downloading the new image.
|
||||
///
|
||||
public func keepCurrentImageWhileLoading(_ enabled: Bool = true) -> Self {
|
||||
options.keepCurrentImageWhileLoading = enabled
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets whether only the first frame from an animated image file should be loaded as a single image.
|
||||
/// - Parameter enabled: Whether the only the first frame should be loaded.
|
||||
/// - Returns: A `KF.Builder` with changes applied.
|
||||
///
|
||||
/// Loading an animated images may take too much memory. It will be useful when you want to display a
|
||||
/// static preview of the first frame from an animated image.
|
||||
///
|
||||
/// This option will be ignored if the target image is not animated image data.
|
||||
///
|
||||
public func onlyLoadFirstFrame(_ enabled: Bool = true) -> Self {
|
||||
options.onlyLoadFirstFrame = enabled
|
||||
return self
|
||||
}
|
||||
|
||||
/// Enables progressive image loading with a specified `ImageProgressive` setting to process the
|
||||
/// progressive JPEG data and display it in a progressive way.
|
||||
/// - Parameter progressive: The progressive settings which is used while loading.
|
||||
/// - Returns: A `KF.Builder` with changes applied.
|
||||
public func progressiveJPEG(_ progressive: ImageProgressive? = .init()) -> Self {
|
||||
options.progressiveJPEG = progressive
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Deprecated
|
||||
extension KF.Builder {
|
||||
/// Starts the loading process of `self` immediately.
|
||||
///
|
||||
/// By default, a `KFImage` will not load its source until the `onAppear` is called. This is a lazily loading
|
||||
/// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a
|
||||
/// flickering since the loading does not happen immediately. Call this method if you want to start the load at once
|
||||
/// could help avoiding the flickering, with some performance trade-off.
|
||||
///
|
||||
/// - Deprecated: This is not necessary anymore since `@StateObject` is used for holding the image data.
|
||||
/// It does nothing now and please just remove it.
|
||||
///
|
||||
/// - Returns: The `Self` value with changes applied.
|
||||
@available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.")
|
||||
public func loadImmediately(_ start: Bool = true) -> Self {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Redirect Handler
|
||||
extension KF {
|
||||
|
||||
/// Represents the detail information when a task redirect happens. It is wrapping necessary information for a
|
||||
/// `ImageDownloadRedirectHandler`. See that protocol for more information.
|
||||
public struct RedirectPayload {
|
||||
|
||||
/// The related session data task when the redirect happens. It is
|
||||
/// the current `SessionDataTask` which triggers this redirect.
|
||||
public let task: SessionDataTask
|
||||
|
||||
/// The response received during redirection.
|
||||
public let response: HTTPURLResponse
|
||||
|
||||
/// The request for redirection which can be modified.
|
||||
public let newRequest: URLRequest
|
||||
|
||||
/// A closure for being called with modified request.
|
||||
public let completionHandler: (URLRequest?) -> Void
|
||||
}
|
||||
}
|
||||
706
Pods/Kingfisher/Sources/General/KFOptionsSetter.swift
generated
Normal file
706
Pods/Kingfisher/Sources/General/KFOptionsSetter.swift
generated
Normal file
@@ -0,0 +1,706 @@
|
||||
//
|
||||
// KFOptionsSetter.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2020/12/22.
|
||||
//
|
||||
// Copyright (c) 2020 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
|
||||
import CoreGraphics
|
||||
|
||||
public protocol KFOptionSetter {
|
||||
var options: KingfisherParsedOptionsInfo { get nonmutating set }
|
||||
|
||||
var onFailureDelegate: Delegate<KingfisherError, Void> { get }
|
||||
var onSuccessDelegate: Delegate<RetrieveImageResult, Void> { get }
|
||||
var onProgressDelegate: Delegate<(Int64, Int64), Void> { get }
|
||||
|
||||
var delegateObserver: AnyObject { get }
|
||||
}
|
||||
|
||||
extension KF.Builder: KFOptionSetter {
|
||||
public var delegateObserver: AnyObject { self }
|
||||
}
|
||||
|
||||
// MARK: - Life cycles
|
||||
extension KFOptionSetter {
|
||||
/// Sets the progress block to current builder.
|
||||
/// - Parameter block: Called when the image downloading progress gets updated. If the response does not contain an
|
||||
/// `expectedContentLength`, this block will not be called. If `block` is `nil`, the callback
|
||||
/// will be reset.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func onProgress(_ block: DownloadProgressBlock?) -> Self {
|
||||
onProgressDelegate.delegate(on: delegateObserver) { (observer, result) in
|
||||
block?(result.0, result.1)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the the done block to current builder.
|
||||
/// - Parameter block: Called when the image task successfully completes and the the image set is done. If `block`
|
||||
/// is `nil`, the callback will be reset.
|
||||
/// - Returns: A `KF.Builder` with changes applied.
|
||||
public func onSuccess(_ block: ((RetrieveImageResult) -> Void)?) -> Self {
|
||||
onSuccessDelegate.delegate(on: delegateObserver) { (observer, result) in
|
||||
block?(result)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the catch block to current builder.
|
||||
/// - Parameter block: Called when an error happens during the image task. If `block`
|
||||
/// is `nil`, the callback will be reset.
|
||||
/// - Returns: A `KF.Builder` with changes applied.
|
||||
public func onFailure(_ block: ((KingfisherError) -> Void)?) -> Self {
|
||||
onFailureDelegate.delegate(on: delegateObserver) { (observer, error) in
|
||||
block?(error)
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Basic options settings.
|
||||
extension KFOptionSetter {
|
||||
/// Sets the target image cache for this task.
|
||||
/// - Parameter cache: The target cache is about to be used for the task.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// Kingfisher will use the associated `ImageCache` object when handling related operations,
|
||||
/// including trying to retrieve the cached images and store the downloaded image to it.
|
||||
///
|
||||
public func targetCache(_ cache: ImageCache) -> Self {
|
||||
options.targetCache = cache
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the target image cache to store the original downloaded image for this task.
|
||||
/// - Parameter cache: The target cache is about to be used for storing the original downloaded image from the task.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// The `ImageCache` for storing and retrieving original images. If `originalCache` is
|
||||
/// contained in the options, it will be preferred for storing and retrieving original images.
|
||||
/// If there is no `.originalCache` in the options, `.targetCache` will be used to store original images.
|
||||
///
|
||||
/// When using KingfisherManager to download and store an image, if `cacheOriginalImage` is
|
||||
/// applied in the option, the original image will be stored to this `originalCache`. At the
|
||||
/// same time, if a requested final image (with processor applied) cannot be found in `targetCache`,
|
||||
/// Kingfisher will try to search the original image to check whether it is already there. If found,
|
||||
/// it will be used and applied with the given processor. It is an optimization for not downloading
|
||||
/// the same image for multiple times.
|
||||
///
|
||||
public func originalCache(_ cache: ImageCache) -> Self {
|
||||
options.originalCache = cache
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the downloader used to perform the image download task.
|
||||
/// - Parameter downloader: The downloader which is about to be used for downloading.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// Kingfisher will use the set `ImageDownloader` object to download the requested images.
|
||||
public func downloader(_ downloader: ImageDownloader) -> Self {
|
||||
options.downloader = downloader
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the download priority for the image task.
|
||||
/// - Parameter priority: The download priority of image download task.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// The `priority` value will be set as the priority of the image download task. The value for it should be
|
||||
/// between 0.0~1.0. You can choose a value between `URLSessionTask.defaultPriority`, `URLSessionTask.lowPriority`
|
||||
/// or `URLSessionTask.highPriority`. If this option not set, the default value (`URLSessionTask.defaultPriority`)
|
||||
/// will be used.
|
||||
public func downloadPriority(_ priority: Float) -> Self {
|
||||
options.downloadPriority = priority
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets whether Kingfisher should ignore the cache and try to start a download task for the image source.
|
||||
/// - Parameter enabled: Enable the force refresh or not.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func forceRefresh(_ enabled: Bool = true) -> Self {
|
||||
options.forceRefresh = enabled
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets whether Kingfisher should try to retrieve the image from memory cache first. If not found, it ignores the
|
||||
/// disk cache and starts a download task for the image source.
|
||||
/// - Parameter enabled: Enable the memory-only cache searching or not.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// This is useful when you want to display a changeable image behind the same url at the same app session, while
|
||||
/// avoiding download it for multiple times.
|
||||
public func fromMemoryCacheOrRefresh(_ enabled: Bool = true) -> Self {
|
||||
options.fromMemoryCacheOrRefresh = enabled
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets whether the image should only be cached in memory but not in disk.
|
||||
/// - Parameter enabled: Whether the image should be only cache in memory or not.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func cacheMemoryOnly(_ enabled: Bool = true) -> Self {
|
||||
options.cacheMemoryOnly = enabled
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets whether Kingfisher should wait for caching operation to be completed before calling the
|
||||
/// `onSuccess` or `onFailure` block.
|
||||
/// - Parameter enabled: Whether Kingfisher should wait for caching operation.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func waitForCache(_ enabled: Bool = true) -> Self {
|
||||
options.waitForCache = enabled
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets whether Kingfisher should only try to retrieve the image from cache, but not from network.
|
||||
/// - Parameter enabled: Whether Kingfisher should only try to retrieve the image from cache.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// If the image is not in cache, the image retrieving will fail with the
|
||||
/// `KingfisherError.cacheError` with `.imageNotExisting` as its reason.
|
||||
public func onlyFromCache(_ enabled: Bool = true) -> Self {
|
||||
options.onlyFromCache = enabled
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets whether the image should be decoded in a background thread before using.
|
||||
/// - Parameter enabled: Whether the image should be decoded in a background thread before using.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// Setting to `true` will decode the downloaded image data and do a off-screen rendering to extract pixel
|
||||
/// information in background. This can speed up display, but will cost more time and memory to prepare the image
|
||||
/// for using.
|
||||
public func backgroundDecode(_ enabled: Bool = true) -> Self {
|
||||
options.backgroundDecode = enabled
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the callback queue which is used as the target queue of dispatch callbacks when retrieving images from
|
||||
/// cache. If not set, Kingfisher will use main queue for callbacks.
|
||||
/// - Parameter queue: The target queue which the cache retrieving callback will be invoked on.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// - Note:
|
||||
/// This option does not affect the callbacks for UI related extension methods or `KFImage` result handlers.
|
||||
/// You will always get the callbacks called from main queue.
|
||||
public func callbackQueue(_ queue: CallbackQueue) -> Self {
|
||||
options.callbackQueue = queue
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the scale factor value when converting retrieved data to an image.
|
||||
/// - Parameter factor: The scale factor value.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// Specify the image scale, instead of your screen scale. You may need to set the correct scale when you dealing
|
||||
/// with 2x or 3x retina images. Otherwise, Kingfisher will convert the data to image object at `scale` 1.0.
|
||||
///
|
||||
public func scaleFactor(_ factor: CGFloat) -> Self {
|
||||
options.scaleFactor = factor
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets whether the original image should be cached even when the original image has been processed by any other
|
||||
/// `ImageProcessor`s.
|
||||
/// - Parameter enabled: Whether the original image should be cached.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// If set and an `ImageProcessor` is used, Kingfisher will try to cache both the final result and original
|
||||
/// image. Kingfisher will have a chance to use the original image when another processor is applied to the same
|
||||
/// resource, instead of downloading it again. You can use `.originalCache` to specify a cache or the original
|
||||
/// images if necessary.
|
||||
///
|
||||
/// The original image will be only cached to disk storage.
|
||||
///
|
||||
public func cacheOriginalImage(_ enabled: Bool = true) -> Self {
|
||||
options.cacheOriginalImage = enabled
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets writing options for an original image on a first write
|
||||
/// - Parameter writingOptions: Options to control the writing of data to a disk storage.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
/// If set, options will be passed the store operation for a new files.
|
||||
///
|
||||
/// This is useful if you want to implement file enctyption on first write - eg [.completeFileProtection]
|
||||
///
|
||||
public func diskStoreWriteOptions(_ writingOptions: Data.WritingOptions) -> Self {
|
||||
options.diskStoreWriteOptions = writingOptions
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets whether the disk storage loading should happen in the same calling queue.
|
||||
/// - Parameter enabled: Whether the disk storage loading should happen in the same calling queue.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// By default, disk storage file loading
|
||||
/// happens in its own queue with an asynchronous dispatch behavior. Although it provides better non-blocking disk
|
||||
/// loading performance, it also causes a flickering when you reload an image from disk, if the image view already
|
||||
/// has an image set.
|
||||
///
|
||||
/// Set this options will stop that flickering by keeping all loading in the same queue (typically the UI queue
|
||||
/// if you are using Kingfisher's extension methods to set an image), with a tradeoff of loading performance.
|
||||
///
|
||||
public func loadDiskFileSynchronously(_ enabled: Bool = true) -> Self {
|
||||
options.loadDiskFileSynchronously = enabled
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets a queue on which the image processing should happen.
|
||||
/// - Parameter queue: The queue on which the image processing should happen.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// By default, Kingfisher uses a pre-defined serial
|
||||
/// queue to process images. Use this option to change this behavior. For example, specify a `.mainCurrentOrAsync`
|
||||
/// to let the image be processed in main queue to prevent a possible flickering (but with a possibility of
|
||||
/// blocking the UI, especially if the processor needs a lot of time to run).
|
||||
public func processingQueue(_ queue: CallbackQueue?) -> Self {
|
||||
options.processingQueue = queue
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the alternative sources that will be used when loading of the original input `Source` fails.
|
||||
/// - Parameter sources: The alternative sources will be used.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// Values of the `sources` array will be used to start a new image loading task if the previous task
|
||||
/// fails due to an error. The image source loading process will stop as soon as a source is loaded successfully.
|
||||
/// If all `sources` are used but the loading is still failing, an `imageSettingError` with
|
||||
/// `alternativeSourcesExhausted` as its reason will be given out in the `catch` block.
|
||||
///
|
||||
/// This is useful if you want to implement a fallback solution for setting image.
|
||||
///
|
||||
/// User cancellation will not trigger the alternative source loading.
|
||||
public func alternativeSources(_ sources: [Source]?) -> Self {
|
||||
options.alternativeSources = sources
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets a retry strategy that will be used when something gets wrong during the image retrieving.
|
||||
/// - Parameter strategy: The provided strategy to define how the retrying should happen.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func retry(_ strategy: RetryStrategy?) -> Self {
|
||||
options.retryStrategy = strategy
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets a retry strategy with a max retry count and retrying interval.
|
||||
/// - Parameters:
|
||||
/// - maxCount: The maximum count before the retry stops.
|
||||
/// - interval: The time interval between each retry attempt.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// This defines the simplest retry strategy, which retry a failing request for several times, with some certain
|
||||
/// interval between each time. For example, `.retry(maxCount: 3, interval: .second(3))` means attempt for at most
|
||||
/// three times, and wait for 3 seconds if a previous retry attempt fails, then start a new attempt.
|
||||
public func retry(maxCount: Int, interval: DelayRetryStrategy.Interval = .seconds(3)) -> Self {
|
||||
let strategy = DelayRetryStrategy(maxRetryCount: maxCount, retryInterval: interval)
|
||||
options.retryStrategy = strategy
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the `Source` should be loaded when user enables Low Data Mode and the original source fails with an
|
||||
/// `NSURLErrorNetworkUnavailableReason.constrained` error.
|
||||
/// - Parameter source: The `Source` will be loaded under low data mode.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// When this option is set, the
|
||||
/// `allowsConstrainedNetworkAccess` property of the request for the original source will be set to `false` and the
|
||||
/// `Source` in associated value will be used to retrieve the image for low data mode. Usually, you can provide a
|
||||
/// low-resolution version of your image or a local image provider to display a placeholder.
|
||||
///
|
||||
/// If not set or the `source` is `nil`, the device Low Data Mode will be ignored and the original source will
|
||||
/// be loaded following the system default behavior, in a normal way.
|
||||
public func lowDataModeSource(_ source: Source?) -> Self {
|
||||
options.lowDataModeSource = source
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets whether the image setting for an image view should happen with transition even when retrieved from cache.
|
||||
/// - Parameter enabled: Enable the force transition or not.
|
||||
/// - Returns: A `Self` with changes applied.
|
||||
public func forceTransition(_ enabled: Bool = true) -> Self {
|
||||
options.forceTransition = enabled
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the image that will be used if an image retrieving task fails.
|
||||
/// - Parameter image: The image that will be used when something goes wrong.
|
||||
/// - Returns: A `Self` with changes applied.
|
||||
///
|
||||
/// If set and an image retrieving error occurred Kingfisher will set provided image (or empty)
|
||||
/// in place of requested one. It's useful when you don't want to show placeholder
|
||||
/// during loading time but wants to use some default image when requests will be failed.
|
||||
///
|
||||
public func onFailureImage(_ image: KFCrossPlatformImage?) -> Self {
|
||||
options.onFailureImage = .some(image)
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Request Modifier
|
||||
extension KFOptionSetter {
|
||||
/// Sets an `ImageDownloadRequestModifier` to change the image download request before it being sent.
|
||||
/// - Parameter modifier: The modifier will be used to change the request before it being sent.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// This is the last chance you can modify the image download request. You can modify the request for some
|
||||
/// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping.
|
||||
///
|
||||
public func requestModifier(_ modifier: AsyncImageDownloadRequestModifier) -> Self {
|
||||
options.requestModifier = modifier
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets a block to change the image download request before it being sent.
|
||||
/// - Parameter modifyBlock: The modifying block will be called to change the request before it being sent.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// This is the last chance you can modify the image download request. You can modify the request for some
|
||||
/// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping.
|
||||
///
|
||||
public func requestModifier(_ modifyBlock: @escaping (inout URLRequest) -> Void) -> Self {
|
||||
options.requestModifier = AnyModifier { r -> URLRequest? in
|
||||
var request = r
|
||||
modifyBlock(&request)
|
||||
return request
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Redirect Handler
|
||||
extension KFOptionSetter {
|
||||
/// The `ImageDownloadRedirectHandler` argument will be used to change the request before redirection.
|
||||
/// This is the possibility you can modify the image download request during redirect. You can modify the request for
|
||||
/// some customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url
|
||||
/// mapping.
|
||||
/// The original redirection request will be sent without any modification by default.
|
||||
/// - Parameter handler: The handler will be used for redirection.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func redirectHandler(_ handler: ImageDownloadRedirectHandler) -> Self {
|
||||
options.redirectHandler = handler
|
||||
return self
|
||||
}
|
||||
|
||||
/// The `block` will be used to change the request before redirection.
|
||||
/// This is the possibility you can modify the image download request during redirect. You can modify the request for
|
||||
/// some customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url
|
||||
/// mapping.
|
||||
/// The original redirection request will be sent without any modification by default.
|
||||
/// - Parameter block: The block will be used for redirection.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func redirectHandler(_ block: @escaping (KF.RedirectPayload) -> Void) -> Self {
|
||||
let redirectHandler = AnyRedirectHandler { (task, response, request, handler) in
|
||||
let payload = KF.RedirectPayload(
|
||||
task: task, response: response, newRequest: request, completionHandler: handler
|
||||
)
|
||||
block(payload)
|
||||
}
|
||||
options.redirectHandler = redirectHandler
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Processor
|
||||
extension KFOptionSetter {
|
||||
|
||||
/// Sets an image processor for the image task. It replaces the current image processor settings.
|
||||
///
|
||||
/// - Parameter processor: The processor you want to use to process the image after it is downloaded.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// - Note:
|
||||
/// To append a processor to current ones instead of replacing them all, use `appendProcessor(_:)`.
|
||||
public func setProcessor(_ processor: ImageProcessor) -> Self {
|
||||
options.processor = processor
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets an array of image processors for the image task. It replaces the current image processor settings.
|
||||
/// - Parameter processors: An array of processors. The processors inside this array will be concatenated one by one
|
||||
/// to form a processor pipeline.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
///
|
||||
/// - Note: To append processors to current ones instead of replacing them all, concatenate them by `|>`, then use
|
||||
/// `appendProcessor(_:)`.
|
||||
public func setProcessors(_ processors: [ImageProcessor]) -> Self {
|
||||
switch processors.count {
|
||||
case 0:
|
||||
options.processor = DefaultImageProcessor.default
|
||||
case 1...:
|
||||
options.processor = processors.dropFirst().reduce(processors[0]) { $0 |> $1 }
|
||||
default:
|
||||
assertionFailure("Never happen")
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/// Appends a processor to the current set processors.
|
||||
/// - Parameter processor: The processor which will be appended to current processor settings.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func appendProcessor(_ processor: ImageProcessor) -> Self {
|
||||
options.processor = options.processor |> processor
|
||||
return self
|
||||
}
|
||||
|
||||
/// Appends a `RoundCornerImageProcessor` to current processors.
|
||||
/// - Parameters:
|
||||
/// - radius: The radius will be applied in processing. Specify a certain point value with `.point`, or a fraction
|
||||
/// of the target image with `.widthFraction`. or `.heightFraction`. For example, given a square image
|
||||
/// with width and height equals, `.widthFraction(0.5)` means use half of the length of size and makes
|
||||
/// the final image a round one.
|
||||
/// - targetSize: Target size of output image should be. If `nil`, the image will keep its original size after processing.
|
||||
/// - corners: The target corners which will be applied rounding.
|
||||
/// - backgroundColor: Background color of the output image. If `nil`, it will use a transparent background.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func roundCorner(
|
||||
radius: Radius,
|
||||
targetSize: CGSize? = nil,
|
||||
roundingCorners corners: RectCorner = .all,
|
||||
backgroundColor: KFCrossPlatformColor? = nil
|
||||
) -> Self
|
||||
{
|
||||
let processor = RoundCornerImageProcessor(
|
||||
radius: radius,
|
||||
targetSize: targetSize,
|
||||
roundingCorners: corners,
|
||||
backgroundColor: backgroundColor
|
||||
)
|
||||
return appendProcessor(processor)
|
||||
}
|
||||
|
||||
/// Appends a `BlurImageProcessor` to current processors.
|
||||
/// - Parameter radius: Blur radius for the simulated Gaussian blur.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func blur(radius: CGFloat) -> Self {
|
||||
appendProcessor(
|
||||
BlurImageProcessor(blurRadius: radius)
|
||||
)
|
||||
}
|
||||
|
||||
/// Appends a `OverlayImageProcessor` to current processors.
|
||||
/// - Parameters:
|
||||
/// - color: Overlay color will be used to overlay the input image.
|
||||
/// - fraction: Fraction will be used when overlay the color to image.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func overlay(color: KFCrossPlatformColor, fraction: CGFloat = 0.5) -> Self {
|
||||
appendProcessor(
|
||||
OverlayImageProcessor(overlay: color, fraction: fraction)
|
||||
)
|
||||
}
|
||||
|
||||
/// Appends a `TintImageProcessor` to current processors.
|
||||
/// - Parameter color: Tint color will be used to tint the input image.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func tint(color: KFCrossPlatformColor) -> Self {
|
||||
appendProcessor(
|
||||
TintImageProcessor(tint: color)
|
||||
)
|
||||
}
|
||||
|
||||
/// Appends a `BlackWhiteProcessor` to current processors.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func blackWhite() -> Self {
|
||||
appendProcessor(
|
||||
BlackWhiteProcessor()
|
||||
)
|
||||
}
|
||||
|
||||
/// Appends a `CroppingImageProcessor` to current processors.
|
||||
/// - Parameters:
|
||||
/// - size: Target size of output image should be.
|
||||
/// - anchor: Anchor point from which the output size should be calculate. The anchor point is consisted by two
|
||||
/// values between 0.0 and 1.0. It indicates a related point in current image.
|
||||
/// See `CroppingImageProcessor.init(size:anchor:)` for more.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func cropping(size: CGSize, anchor: CGPoint = .init(x: 0.5, y: 0.5)) -> Self {
|
||||
appendProcessor(
|
||||
CroppingImageProcessor(size: size, anchor: anchor)
|
||||
)
|
||||
}
|
||||
|
||||
/// Appends a `DownsamplingImageProcessor` to current processors.
|
||||
///
|
||||
/// Compared to `ResizingImageProcessor`, the `DownsamplingImageProcessor` does not render the original images and
|
||||
/// then resize it. Instead, it downsamples the input data directly to a thumbnail image. So it is a more efficient
|
||||
/// than `ResizingImageProcessor`. Prefer to use `DownsamplingImageProcessor` as possible
|
||||
/// as you can than the `ResizingImageProcessor`.
|
||||
///
|
||||
/// Only CG-based images are supported. Animated images (like GIF) is not supported.
|
||||
///
|
||||
/// - Parameter size: Target size of output image should be. It should be smaller than the size of input image.
|
||||
/// If it is larger, the result image will be the same size of input data without downsampling.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func downsampling(size: CGSize) -> Self {
|
||||
let processor = DownsamplingImageProcessor(size: size)
|
||||
if options.processor == DefaultImageProcessor.default {
|
||||
return setProcessor(processor)
|
||||
} else {
|
||||
return appendProcessor(processor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Appends a `ResizingImageProcessor` to current processors.
|
||||
///
|
||||
/// If you need to resize a data represented image to a smaller size, use `DownsamplingImageProcessor`
|
||||
/// instead, which is more efficient and uses less memory.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - referenceSize: The reference size for resizing operation in point.
|
||||
/// - mode: Target content mode of output image should be. Default is `.none`.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func resizing(referenceSize: CGSize, mode: ContentMode = .none) -> Self {
|
||||
appendProcessor(
|
||||
ResizingImageProcessor(referenceSize: referenceSize, mode: mode)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Cache Serializer
|
||||
extension KFOptionSetter {
|
||||
|
||||
/// Uses a given `CacheSerializer` to convert some data to an image object for retrieving from disk cache or vice
|
||||
/// versa for storing to disk cache.
|
||||
/// - Parameter cacheSerializer: The `CacheSerializer` which will be used.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func serialize(by cacheSerializer: CacheSerializer) -> Self {
|
||||
options.cacheSerializer = cacheSerializer
|
||||
return self
|
||||
}
|
||||
|
||||
/// Uses a given format to serializer the image data to disk. It converts the image object to the give data format.
|
||||
/// - Parameters:
|
||||
/// - format: The desired data encoding format when store the image on disk.
|
||||
/// - jpegCompressionQuality: If the format is `.JPEG`, it specify the compression quality when converting the
|
||||
/// image to a JPEG data. Otherwise, it is ignored.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func serialize(as format: ImageFormat, jpegCompressionQuality: CGFloat? = nil) -> Self {
|
||||
let cacheSerializer: FormatIndicatedCacheSerializer
|
||||
switch format {
|
||||
case .JPEG:
|
||||
cacheSerializer = .jpeg(compressionQuality: jpegCompressionQuality ?? 1.0)
|
||||
case .PNG:
|
||||
cacheSerializer = .png
|
||||
case .GIF:
|
||||
cacheSerializer = .gif
|
||||
case .unknown:
|
||||
cacheSerializer = .png
|
||||
}
|
||||
options.cacheSerializer = cacheSerializer
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image Modifier
|
||||
extension KFOptionSetter {
|
||||
|
||||
/// Sets an `ImageModifier` to the image task. Use this to modify the fetched image object properties if needed.
|
||||
///
|
||||
/// If the image was fetched directly from the downloader, the modifier will run directly after the
|
||||
/// `ImageProcessor`. If the image is being fetched from a cache, the modifier will run after the `CacheSerializer`.
|
||||
/// - Parameter modifier: The `ImageModifier` which will be used to modify the image object.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func imageModifier(_ modifier: ImageModifier?) -> Self {
|
||||
options.imageModifier = modifier
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets a block to modify the image object. Use this to modify the fetched image object properties if needed.
|
||||
///
|
||||
/// If the image was fetched directly from the downloader, the modifier block will run directly after the
|
||||
/// `ImageProcessor`. If the image is being fetched from a cache, the modifier will run after the `CacheSerializer`.
|
||||
///
|
||||
/// - Parameter block: The block which is used to modify the image object.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func imageModifier(_ block: @escaping (inout KFCrossPlatformImage) throws -> Void) -> Self {
|
||||
let modifier = AnyImageModifier { image -> KFCrossPlatformImage in
|
||||
var image = image
|
||||
try block(&image)
|
||||
return image
|
||||
}
|
||||
options.imageModifier = modifier
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Cache Expiration
|
||||
extension KFOptionSetter {
|
||||
|
||||
/// Sets the expiration setting for memory cache of this image task.
|
||||
///
|
||||
/// By default, the underlying `MemoryStorage.Backend` uses the
|
||||
/// expiration in its config for all items. If set, the `MemoryStorage.Backend` will use this value to overwrite
|
||||
/// the config setting for this caching item.
|
||||
///
|
||||
/// - Parameter expiration: The expiration setting used in cache storage.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func memoryCacheExpiration(_ expiration: StorageExpiration?) -> Self {
|
||||
options.memoryCacheExpiration = expiration
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the expiration extending setting for memory cache. The item expiration time will be incremented by this
|
||||
/// value after access.
|
||||
///
|
||||
/// By default, the underlying `MemoryStorage.Backend` uses the initial cache expiration as extending
|
||||
/// value: .cacheTime.
|
||||
///
|
||||
/// To disable extending option at all, sets `.none` to it.
|
||||
///
|
||||
/// - Parameter extending: The expiration extending setting used in cache storage.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func memoryCacheAccessExtending(_ extending: ExpirationExtending) -> Self {
|
||||
options.memoryCacheAccessExtendingExpiration = extending
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the expiration setting for disk cache of this image task.
|
||||
///
|
||||
/// By default, the underlying `DiskStorage.Backend` uses the expiration in its config for all items. If set,
|
||||
/// the `DiskStorage.Backend` will use this value to overwrite the config setting for this caching item.
|
||||
///
|
||||
/// - Parameter expiration: The expiration setting used in cache storage.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func diskCacheExpiration(_ expiration: StorageExpiration?) -> Self {
|
||||
options.diskCacheExpiration = expiration
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets the expiration extending setting for disk cache. The item expiration time will be incremented by this
|
||||
/// value after access.
|
||||
///
|
||||
/// By default, the underlying `DiskStorage.Backend` uses the initial cache expiration as extending
|
||||
/// value: .cacheTime.
|
||||
///
|
||||
/// To disable extending option at all, sets `.none` to it.
|
||||
///
|
||||
/// - Parameter extending: The expiration extending setting used in cache storage.
|
||||
/// - Returns: A `Self` value with changes applied.
|
||||
public func diskCacheAccessExtending(_ extending: ExpirationExtending) -> Self {
|
||||
options.diskCacheAccessExtendingExpiration = extending
|
||||
return self
|
||||
}
|
||||
}
|
||||
106
Pods/Kingfisher/Sources/General/Kingfisher.swift
generated
Normal file
106
Pods/Kingfisher/Sources/General/Kingfisher.swift
generated
Normal file
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// Kingfisher.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 16/9/14.
|
||||
//
|
||||
// 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
|
||||
import ImageIO
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
public typealias KFCrossPlatformImage = NSImage
|
||||
public typealias KFCrossPlatformView = NSView
|
||||
public typealias KFCrossPlatformColor = NSColor
|
||||
public typealias KFCrossPlatformImageView = NSImageView
|
||||
public typealias KFCrossPlatformButton = NSButton
|
||||
#else
|
||||
import UIKit
|
||||
public typealias KFCrossPlatformImage = UIImage
|
||||
public typealias KFCrossPlatformColor = UIColor
|
||||
#if !os(watchOS)
|
||||
public typealias KFCrossPlatformImageView = UIImageView
|
||||
public typealias KFCrossPlatformView = UIView
|
||||
public typealias KFCrossPlatformButton = UIButton
|
||||
#if canImport(TVUIKit)
|
||||
import TVUIKit
|
||||
#endif
|
||||
#if canImport(CarPlay) && !targetEnvironment(macCatalyst)
|
||||
import CarPlay
|
||||
#endif
|
||||
#else
|
||||
import WatchKit
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/// Wrapper for Kingfisher compatible types. This type provides an extension point for
|
||||
/// convenience methods in Kingfisher.
|
||||
public struct KingfisherWrapper<Base> {
|
||||
public let base: Base
|
||||
public init(_ base: Base) {
|
||||
self.base = base
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an object type that is compatible with Kingfisher. You can use `kf` property to get a
|
||||
/// value in the namespace of Kingfisher.
|
||||
public protocol KingfisherCompatible: AnyObject { }
|
||||
|
||||
/// Represents a value type that is compatible with Kingfisher. You can use `kf` property to get a
|
||||
/// value in the namespace of Kingfisher.
|
||||
public protocol KingfisherCompatibleValue {}
|
||||
|
||||
extension KingfisherCompatible {
|
||||
/// Gets a namespace holder for Kingfisher compatible types.
|
||||
public var kf: KingfisherWrapper<Self> {
|
||||
get { return KingfisherWrapper(self) }
|
||||
set { }
|
||||
}
|
||||
}
|
||||
|
||||
extension KingfisherCompatibleValue {
|
||||
/// Gets a namespace holder for Kingfisher compatible types.
|
||||
public var kf: KingfisherWrapper<Self> {
|
||||
get { return KingfisherWrapper(self) }
|
||||
set { }
|
||||
}
|
||||
}
|
||||
|
||||
extension KFCrossPlatformImage: KingfisherCompatible { }
|
||||
#if !os(watchOS)
|
||||
extension KFCrossPlatformImageView: KingfisherCompatible { }
|
||||
extension KFCrossPlatformButton: KingfisherCompatible { }
|
||||
extension NSTextAttachment: KingfisherCompatible { }
|
||||
#else
|
||||
extension WKInterfaceImage: KingfisherCompatible { }
|
||||
#endif
|
||||
|
||||
#if os(tvOS) && canImport(TVUIKit)
|
||||
@available(tvOS 12.0, *)
|
||||
extension TVMonogramView: KingfisherCompatible { }
|
||||
#endif
|
||||
|
||||
#if canImport(CarPlay) && !targetEnvironment(macCatalyst)
|
||||
@available(iOS 14.0, *)
|
||||
extension CPListItem: KingfisherCompatible { }
|
||||
#endif
|
||||
469
Pods/Kingfisher/Sources/General/KingfisherError.swift
generated
Normal file
469
Pods/Kingfisher/Sources/General/KingfisherError.swift
generated
Normal file
@@ -0,0 +1,469 @@
|
||||
//
|
||||
// KingfisherError.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2018/09/26.
|
||||
//
|
||||
// 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
|
||||
|
||||
extension Never {}
|
||||
|
||||
/// Represents all the errors which can happen in Kingfisher framework.
|
||||
/// Kingfisher related methods always throw a `KingfisherError` or invoke the callback with `KingfisherError`
|
||||
/// as its error type. To handle errors from Kingfisher, you switch over the error to get a reason catalog,
|
||||
/// then switch over the reason to know error detail.
|
||||
public enum KingfisherError: Error {
|
||||
|
||||
// MARK: Error Reason Types
|
||||
|
||||
/// Represents the error reason during networking request phase.
|
||||
///
|
||||
/// - emptyRequest: The request is empty. Code 1001.
|
||||
/// - invalidURL: The URL of request is invalid. Code 1002.
|
||||
/// - taskCancelled: The downloading task is cancelled by user. Code 1003.
|
||||
public enum RequestErrorReason {
|
||||
|
||||
/// The request is empty. Code 1001.
|
||||
case emptyRequest
|
||||
|
||||
/// The URL of request is invalid. Code 1002.
|
||||
/// - request: The request is tend to be sent but its URL is invalid.
|
||||
case invalidURL(request: URLRequest)
|
||||
|
||||
/// The downloading task is cancelled by user. Code 1003.
|
||||
/// - task: The session data task which is cancelled.
|
||||
/// - token: The cancel token which is used for cancelling the task.
|
||||
case taskCancelled(task: SessionDataTask, token: SessionDataTask.CancelToken)
|
||||
}
|
||||
|
||||
/// Represents the error reason during networking response phase.
|
||||
///
|
||||
/// - invalidURLResponse: The response is not a valid URL response. Code 2001.
|
||||
/// - invalidHTTPStatusCode: The response contains an invalid HTTP status code. Code 2002.
|
||||
/// - URLSessionError: An error happens in the system URL session. Code 2003.
|
||||
/// - dataModifyingFailed: Data modifying fails on returning a valid data. Code 2004.
|
||||
/// - noURLResponse: The task is done but no URL response found. Code 2005.
|
||||
public enum ResponseErrorReason {
|
||||
|
||||
/// The response is not a valid URL response. Code 2001.
|
||||
/// - response: The received invalid URL response.
|
||||
/// The response is expected to be an HTTP response, but it is not.
|
||||
case invalidURLResponse(response: URLResponse)
|
||||
|
||||
/// The response contains an invalid HTTP status code. Code 2002.
|
||||
/// - Note:
|
||||
/// By default, status code 200..<400 is recognized as valid. You can override
|
||||
/// this behavior by conforming to the `ImageDownloaderDelegate`.
|
||||
/// - response: The received response.
|
||||
case invalidHTTPStatusCode(response: HTTPURLResponse)
|
||||
|
||||
/// An error happens in the system URL session. Code 2003.
|
||||
/// - error: The underlying URLSession error object.
|
||||
case URLSessionError(error: Error)
|
||||
|
||||
/// Data modifying fails on returning a valid data. Code 2004.
|
||||
/// - task: The failed task.
|
||||
case dataModifyingFailed(task: SessionDataTask)
|
||||
|
||||
/// The task is done but no URL response found. Code 2005.
|
||||
/// - task: The failed task.
|
||||
case noURLResponse(task: SessionDataTask)
|
||||
|
||||
/// The task is cancelled by `ImageDownloaderDelegate` due to the `.cancel` response disposition is
|
||||
/// specified by the delegate method. Code 2006.
|
||||
case cancelledByDelegate(response: URLResponse)
|
||||
}
|
||||
|
||||
/// Represents the error reason during Kingfisher caching system.
|
||||
///
|
||||
/// - fileEnumeratorCreationFailed: Cannot create a file enumerator for a certain disk URL. Code 3001.
|
||||
/// - invalidFileEnumeratorContent: Cannot get correct file contents from a file enumerator. Code 3002.
|
||||
/// - invalidURLResource: The file at target URL exists, but its URL resource is unavailable. Code 3003.
|
||||
/// - cannotLoadDataFromDisk: The file at target URL exists, but the data cannot be loaded from it. Code 3004.
|
||||
/// - cannotCreateDirectory: Cannot create a folder at a given path. Code 3005.
|
||||
/// - imageNotExisting: The requested image does not exist in cache. Code 3006.
|
||||
/// - cannotConvertToData: Cannot convert an object to data for storing. Code 3007.
|
||||
/// - cannotSerializeImage: Cannot serialize an image to data for storing. Code 3008.
|
||||
/// - cannotCreateCacheFile: Cannot create the cache file at a certain fileURL under a key. Code 3009.
|
||||
/// - cannotSetCacheFileAttribute: Cannot set file attributes to a cached file. Code 3010.
|
||||
public enum CacheErrorReason {
|
||||
|
||||
/// Cannot create a file enumerator for a certain disk URL. Code 3001.
|
||||
/// - url: The target disk URL from which the file enumerator should be created.
|
||||
case fileEnumeratorCreationFailed(url: URL)
|
||||
|
||||
/// Cannot get correct file contents from a file enumerator. Code 3002.
|
||||
/// - url: The target disk URL from which the content of a file enumerator should be got.
|
||||
case invalidFileEnumeratorContent(url: URL)
|
||||
|
||||
/// The file at target URL exists, but its URL resource is unavailable. Code 3003.
|
||||
/// - error: The underlying error thrown by file manager.
|
||||
/// - key: The key used to getting the resource from cache.
|
||||
/// - url: The disk URL where the target cached file exists.
|
||||
case invalidURLResource(error: Error, key: String, url: URL)
|
||||
|
||||
/// The file at target URL exists, but the data cannot be loaded from it. Code 3004.
|
||||
/// - url: The disk URL where the target cached file exists.
|
||||
/// - error: The underlying error which describes why this error happens.
|
||||
case cannotLoadDataFromDisk(url: URL, error: Error)
|
||||
|
||||
/// Cannot create a folder at a given path. Code 3005.
|
||||
/// - path: The disk path where the directory creating operation fails.
|
||||
/// - error: The underlying error which describes why this error happens.
|
||||
case cannotCreateDirectory(path: String, error: Error)
|
||||
|
||||
/// The requested image does not exist in cache. Code 3006.
|
||||
/// - key: Key of the requested image in cache.
|
||||
case imageNotExisting(key: String)
|
||||
|
||||
/// Cannot convert an object to data for storing. Code 3007.
|
||||
/// - object: The object which needs be convert to data.
|
||||
case cannotConvertToData(object: Any, error: Error)
|
||||
|
||||
/// Cannot serialize an image to data for storing. Code 3008.
|
||||
/// - image: The input image needs to be serialized to cache.
|
||||
/// - original: The original image data, if exists.
|
||||
/// - serializer: The `CacheSerializer` used for the image serializing.
|
||||
case cannotSerializeImage(image: KFCrossPlatformImage?, original: Data?, serializer: CacheSerializer)
|
||||
|
||||
/// Cannot create the cache file at a certain fileURL under a key. Code 3009.
|
||||
/// - fileURL: The url where the cache file should be created.
|
||||
/// - key: The cache key used for the cache. When caching a file through `KingfisherManager` and Kingfisher's
|
||||
/// extension method, it is the resolved cache key based on your input `Source` and the image processors.
|
||||
/// - data: The data to be cached.
|
||||
/// - error: The underlying error originally thrown by Foundation when writing the `data` to the disk file at
|
||||
/// `fileURL`.
|
||||
case cannotCreateCacheFile(fileURL: URL, key: String, data: Data, error: Error)
|
||||
|
||||
/// Cannot set file attributes to a cached file. Code 3010.
|
||||
/// - filePath: The path of target cache file.
|
||||
/// - attributes: The file attribute to be set to the target file.
|
||||
/// - error: The underlying error originally thrown by Foundation when setting the `attributes` to the disk
|
||||
/// file at `filePath`.
|
||||
case cannotSetCacheFileAttribute(filePath: String, attributes: [FileAttributeKey : Any], error: Error)
|
||||
|
||||
/// The disk storage of cache is not ready. Code 3011.
|
||||
///
|
||||
/// This is usually due to extremely lack of space on disk storage, and
|
||||
/// Kingfisher failed even when creating the cache folder. The disk storage will be in unusable state. Normally,
|
||||
/// ask user to free some spaces and restart the app to make the disk storage work again.
|
||||
/// - cacheURL: The intended URL which should be the storage folder.
|
||||
case diskStorageIsNotReady(cacheURL: URL)
|
||||
}
|
||||
|
||||
|
||||
/// Represents the error reason during image processing phase.
|
||||
///
|
||||
/// - processingFailed: Image processing fails. There is no valid output image from the processor. Code 4001.
|
||||
public enum ProcessorErrorReason {
|
||||
/// Image processing fails. There is no valid output image from the processor. Code 4001.
|
||||
/// - processor: The `ImageProcessor` used to process the image or its data in `item`.
|
||||
/// - item: The image or its data content.
|
||||
case processingFailed(processor: ImageProcessor, item: ImageProcessItem)
|
||||
}
|
||||
|
||||
/// Represents the error reason during image setting in a view related class.
|
||||
///
|
||||
/// - emptySource: The input resource is empty or `nil`. Code 5001.
|
||||
/// - notCurrentSourceTask: The source task is finished, but it is not the one expected now. Code 5002.
|
||||
/// - dataProviderError: An error happens during getting data from an `ImageDataProvider`. Code 5003.
|
||||
public enum ImageSettingErrorReason {
|
||||
|
||||
/// The input resource is empty or `nil`. Code 5001.
|
||||
case emptySource
|
||||
|
||||
/// The resource task is finished, but it is not the one expected now. This usually happens when you set another
|
||||
/// resource on the view without cancelling the current on-going one. The previous setting task will fail with
|
||||
/// this `.notCurrentSourceTask` error when a result got, regardless of it being successful or not for that task.
|
||||
/// The result of this original task is contained in the associated value.
|
||||
/// Code 5002.
|
||||
/// - result: The `RetrieveImageResult` if the source task is finished without problem. `nil` if an error
|
||||
/// happens.
|
||||
/// - error: The `Error` if an issue happens during image setting task. `nil` if the task finishes without
|
||||
/// problem.
|
||||
/// - source: The original source value of the task.
|
||||
case notCurrentSourceTask(result: RetrieveImageResult?, error: Error?, source: Source)
|
||||
|
||||
/// An error happens during getting data from an `ImageDataProvider`. Code 5003.
|
||||
case dataProviderError(provider: ImageDataProvider, error: Error)
|
||||
|
||||
/// No more alternative `Source` can be used in current loading process. It means that the
|
||||
/// `.alternativeSources` are used and Kingfisher tried to recovery from the original error, but still
|
||||
/// fails for all the given alternative sources. The associated value holds all the errors encountered during
|
||||
/// the load process, including the original source loading error and all the alternative sources errors.
|
||||
/// Code 5004.
|
||||
case alternativeSourcesExhausted([PropagationError])
|
||||
}
|
||||
|
||||
// MARK: Member Cases
|
||||
|
||||
/// Represents the error reason during networking request phase.
|
||||
case requestError(reason: RequestErrorReason)
|
||||
/// Represents the error reason during networking response phase.
|
||||
case responseError(reason: ResponseErrorReason)
|
||||
/// Represents the error reason during Kingfisher caching system.
|
||||
case cacheError(reason: CacheErrorReason)
|
||||
/// Represents the error reason during image processing phase.
|
||||
case processorError(reason: ProcessorErrorReason)
|
||||
/// Represents the error reason during image setting in a view related class.
|
||||
case imageSettingError(reason: ImageSettingErrorReason)
|
||||
|
||||
// MARK: Helper Properties & Methods
|
||||
|
||||
/// Helper property to check whether this error is a `RequestErrorReason.taskCancelled` or not.
|
||||
public var isTaskCancelled: Bool {
|
||||
if case .requestError(reason: .taskCancelled) = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// Helper method to check whether this error is a `ResponseErrorReason.invalidHTTPStatusCode` and the
|
||||
/// associated value is a given status code.
|
||||
///
|
||||
/// - Parameter code: The given status code.
|
||||
/// - Returns: If `self` is a `ResponseErrorReason.invalidHTTPStatusCode` error
|
||||
/// and its status code equals to `code`, `true` is returned. Otherwise, `false`.
|
||||
public func isInvalidResponseStatusCode(_ code: Int) -> Bool {
|
||||
if case .responseError(reason: .invalidHTTPStatusCode(let response)) = self {
|
||||
return response.statusCode == code
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public var isInvalidResponseStatusCode: Bool {
|
||||
if case .responseError(reason: .invalidHTTPStatusCode) = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// Helper property to check whether this error is a `ImageSettingErrorReason.notCurrentSourceTask` or not.
|
||||
/// When a new image setting task starts while the old one is still running, the new task identifier will be
|
||||
/// set and the old one is overwritten. A `.notCurrentSourceTask` error will be raised when the old task finishes
|
||||
/// to let you know the setting process finishes with a certain result, but the image view or button is not set.
|
||||
public var isNotCurrentTask: Bool {
|
||||
if case .imageSettingError(reason: .notCurrentSourceTask(_, _, _)) = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var isLowDataModeConstrained: Bool {
|
||||
if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *),
|
||||
case .responseError(reason: .URLSessionError(let sessionError)) = self,
|
||||
let urlError = sessionError as? URLError,
|
||||
urlError.networkUnavailableReason == .constrained
|
||||
{
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - LocalizedError Conforming
|
||||
extension KingfisherError: LocalizedError {
|
||||
|
||||
/// A localized message describing what error occurred.
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .requestError(let reason): return reason.errorDescription
|
||||
case .responseError(let reason): return reason.errorDescription
|
||||
case .cacheError(let reason): return reason.errorDescription
|
||||
case .processorError(let reason): return reason.errorDescription
|
||||
case .imageSettingError(let reason): return reason.errorDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - CustomNSError Conforming
|
||||
extension KingfisherError: CustomNSError {
|
||||
|
||||
/// The error domain of `KingfisherError`. All errors from Kingfisher is under this domain.
|
||||
public static let domain = "com.onevcat.Kingfisher.Error"
|
||||
|
||||
/// The error code within the given domain.
|
||||
public var errorCode: Int {
|
||||
switch self {
|
||||
case .requestError(let reason): return reason.errorCode
|
||||
case .responseError(let reason): return reason.errorCode
|
||||
case .cacheError(let reason): return reason.errorCode
|
||||
case .processorError(let reason): return reason.errorCode
|
||||
case .imageSettingError(let reason): return reason.errorCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension KingfisherError.RequestErrorReason {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .emptyRequest:
|
||||
return "The request is empty or `nil`."
|
||||
case .invalidURL(let request):
|
||||
return "The request contains an invalid or empty URL. Request: \(request)."
|
||||
case .taskCancelled(let task, let token):
|
||||
return "The session task was cancelled. Task: \(task), cancel token: \(token)."
|
||||
}
|
||||
}
|
||||
|
||||
var errorCode: Int {
|
||||
switch self {
|
||||
case .emptyRequest: return 1001
|
||||
case .invalidURL: return 1002
|
||||
case .taskCancelled: return 1003
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension KingfisherError.ResponseErrorReason {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidURLResponse(let response):
|
||||
return "The URL response is invalid: \(response)"
|
||||
case .invalidHTTPStatusCode(let response):
|
||||
return "The HTTP status code in response is invalid. Code: \(response.statusCode), response: \(response)."
|
||||
case .URLSessionError(let error):
|
||||
return "A URL session error happened. The underlying error: \(error)"
|
||||
case .dataModifyingFailed(let task):
|
||||
return "The data modifying delegate returned `nil` for the downloaded data. Task: \(task)."
|
||||
case .noURLResponse(let task):
|
||||
return "No URL response received. Task: \(task)."
|
||||
case .cancelledByDelegate(let response):
|
||||
return "The downloading task is cancelled by the downloader delegate. Response: \(response)."
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var errorCode: Int {
|
||||
switch self {
|
||||
case .invalidURLResponse: return 2001
|
||||
case .invalidHTTPStatusCode: return 2002
|
||||
case .URLSessionError: return 2003
|
||||
case .dataModifyingFailed: return 2004
|
||||
case .noURLResponse: return 2005
|
||||
case .cancelledByDelegate: return 2006
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension KingfisherError.CacheErrorReason {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .fileEnumeratorCreationFailed(let url):
|
||||
return "Cannot create file enumerator for URL: \(url)."
|
||||
case .invalidFileEnumeratorContent(let url):
|
||||
return "Cannot get contents from the file enumerator at URL: \(url)."
|
||||
case .invalidURLResource(let error, let key, let url):
|
||||
return "Cannot get URL resource values or data for the given URL: \(url). " +
|
||||
"Cache key: \(key). Underlying error: \(error)"
|
||||
case .cannotLoadDataFromDisk(let url, let error):
|
||||
return "Cannot load data from disk at URL: \(url). Underlying error: \(error)"
|
||||
case .cannotCreateDirectory(let path, let error):
|
||||
return "Cannot create directory at given path: Path: \(path). Underlying error: \(error)"
|
||||
case .imageNotExisting(let key):
|
||||
return "The image is not in cache, but you requires it should only be " +
|
||||
"from cache by enabling the `.onlyFromCache` option. Key: \(key)."
|
||||
case .cannotConvertToData(let object, let error):
|
||||
return "Cannot convert the input object to a `Data` object when storing it to disk cache. " +
|
||||
"Object: \(object). Underlying error: \(error)"
|
||||
case .cannotSerializeImage(let image, let originalData, let serializer):
|
||||
return "Cannot serialize an image due to the cache serializer returning `nil`. " +
|
||||
"Image: \(String(describing:image)), original data: \(String(describing: originalData)), " +
|
||||
"serializer: \(serializer)."
|
||||
case .cannotCreateCacheFile(let fileURL, let key, let data, let error):
|
||||
return "Cannot create cache file at url: \(fileURL), key: \(key), data length: \(data.count). " +
|
||||
"Underlying foundation error: \(error)."
|
||||
case .cannotSetCacheFileAttribute(let filePath, let attributes, let error):
|
||||
return "Cannot set file attribute for the cache file at path: \(filePath), attributes: \(attributes)." +
|
||||
"Underlying foundation error: \(error)."
|
||||
case .diskStorageIsNotReady(let cacheURL):
|
||||
return "The disk storage is not ready to use yet at URL: '\(cacheURL)'. " +
|
||||
"This is usually caused by extremely lack of disk space. Ask users to free up some space and restart the app."
|
||||
}
|
||||
}
|
||||
|
||||
var errorCode: Int {
|
||||
switch self {
|
||||
case .fileEnumeratorCreationFailed: return 3001
|
||||
case .invalidFileEnumeratorContent: return 3002
|
||||
case .invalidURLResource: return 3003
|
||||
case .cannotLoadDataFromDisk: return 3004
|
||||
case .cannotCreateDirectory: return 3005
|
||||
case .imageNotExisting: return 3006
|
||||
case .cannotConvertToData: return 3007
|
||||
case .cannotSerializeImage: return 3008
|
||||
case .cannotCreateCacheFile: return 3009
|
||||
case .cannotSetCacheFileAttribute: return 3010
|
||||
case .diskStorageIsNotReady: return 3011
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension KingfisherError.ProcessorErrorReason {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .processingFailed(let processor, let item):
|
||||
return "Processing image failed. Processor: \(processor). Processing item: \(item)."
|
||||
}
|
||||
}
|
||||
|
||||
var errorCode: Int {
|
||||
switch self {
|
||||
case .processingFailed: return 4001
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension KingfisherError.ImageSettingErrorReason {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .emptySource:
|
||||
return "The input resource is empty."
|
||||
case .notCurrentSourceTask(let result, let error, let resource):
|
||||
if let result = result {
|
||||
return "Retrieving resource succeeded, but this source is " +
|
||||
"not the one currently expected. Result: \(result). Resource: \(resource)."
|
||||
} else if let error = error {
|
||||
return "Retrieving resource failed, and this resource is " +
|
||||
"not the one currently expected. Error: \(error). Resource: \(resource)."
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case .dataProviderError(let provider, let error):
|
||||
return "Image data provider fails to provide data. Provider: \(provider), error: \(error)"
|
||||
case .alternativeSourcesExhausted(let errors):
|
||||
return "Image setting from alternaive sources failed: \(errors)"
|
||||
}
|
||||
}
|
||||
|
||||
var errorCode: Int {
|
||||
switch self {
|
||||
case .emptySource: return 5001
|
||||
case .notCurrentSourceTask: return 5002
|
||||
case .dataProviderError: return 5003
|
||||
case .alternativeSourcesExhausted: return 5004
|
||||
}
|
||||
}
|
||||
}
|
||||
802
Pods/Kingfisher/Sources/General/KingfisherManager.swift
generated
Normal file
802
Pods/Kingfisher/Sources/General/KingfisherManager.swift
generated
Normal file
@@ -0,0 +1,802 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
/// 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
400
Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift
generated
Normal file
400
Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift
generated
Normal file
@@ -0,0 +1,400 @@
|
||||
//
|
||||
// KingfisherOptionsInfo.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 15/4/23.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
|
||||
/// KingfisherOptionsInfo is a typealias for [KingfisherOptionsInfoItem].
|
||||
/// You can use the enum of option item with value to control some behaviors of Kingfisher.
|
||||
public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem]
|
||||
|
||||
extension Array where Element == KingfisherOptionsInfoItem {
|
||||
static let empty: KingfisherOptionsInfo = []
|
||||
}
|
||||
|
||||
/// Represents the available option items could be used in `KingfisherOptionsInfo`.
|
||||
public enum KingfisherOptionsInfoItem {
|
||||
|
||||
/// Kingfisher will use the associated `ImageCache` object when handling related operations,
|
||||
/// including trying to retrieve the cached images and store the downloaded image to it.
|
||||
case targetCache(ImageCache)
|
||||
|
||||
/// The `ImageCache` for storing and retrieving original images. If `originalCache` is
|
||||
/// contained in the options, it will be preferred for storing and retrieving original images.
|
||||
/// If there is no `.originalCache` in the options, `.targetCache` will be used to store original images.
|
||||
///
|
||||
/// When using KingfisherManager to download and store an image, if `cacheOriginalImage` is
|
||||
/// applied in the option, the original image will be stored to this `originalCache`. At the
|
||||
/// same time, if a requested final image (with processor applied) cannot be found in `targetCache`,
|
||||
/// Kingfisher will try to search the original image to check whether it is already there. If found,
|
||||
/// it will be used and applied with the given processor. It is an optimization for not downloading
|
||||
/// the same image for multiple times.
|
||||
case originalCache(ImageCache)
|
||||
|
||||
/// Kingfisher will use the associated `ImageDownloader` object to download the requested images.
|
||||
case downloader(ImageDownloader)
|
||||
|
||||
/// Member for animation transition when using `UIImageView`. Kingfisher will use the `ImageTransition` of
|
||||
/// this enum to animate the image in if it is downloaded from web. The transition will not happen when the
|
||||
/// image is retrieved from either memory or disk cache by default. If you need to do the transition even when
|
||||
/// the image being retrieved from cache, set `.forceRefresh` as well.
|
||||
case transition(ImageTransition)
|
||||
|
||||
/// Associated `Float` value will be set as the priority of image download task. The value for it should be
|
||||
/// between 0.0~1.0. If this option not set, the default value (`URLSessionTask.defaultPriority`) will be used.
|
||||
case downloadPriority(Float)
|
||||
|
||||
/// If set, Kingfisher will ignore the cache and try to start a download task for the image source.
|
||||
case forceRefresh
|
||||
|
||||
/// If set, Kingfisher will try to retrieve the image from memory cache first. If the image is not in memory
|
||||
/// cache, then it will ignore the disk cache but download the image again from network. This is useful when
|
||||
/// you want to display a changeable image behind the same url at the same app session, while avoiding download
|
||||
/// it for multiple times.
|
||||
case fromMemoryCacheOrRefresh
|
||||
|
||||
/// If set, setting the image to an image view will happen with transition even when retrieved from cache.
|
||||
/// See `.transition` option for more.
|
||||
case forceTransition
|
||||
|
||||
/// If set, Kingfisher will only cache the value in memory but not in disk.
|
||||
case cacheMemoryOnly
|
||||
|
||||
/// If set, Kingfisher will wait for caching operation to be completed before calling the completion block.
|
||||
case waitForCache
|
||||
|
||||
/// If set, Kingfisher will only try to retrieve the image from cache, but not from network. If the image is not in
|
||||
/// cache, the image retrieving will fail with the `KingfisherError.cacheError` with `.imageNotExisting` as its
|
||||
/// reason.
|
||||
case onlyFromCache
|
||||
|
||||
/// Decode the image in background thread before using. It will decode the downloaded image data and do a off-screen
|
||||
/// rendering to extract pixel information in background. This can speed up display, but will cost more time to
|
||||
/// prepare the image for using.
|
||||
case backgroundDecode
|
||||
|
||||
/// The associated value will be used as the target queue of dispatch callbacks when retrieving images from
|
||||
/// cache. If not set, Kingfisher will use `.mainCurrentOrAsync` for callbacks.
|
||||
///
|
||||
/// - Note:
|
||||
/// This option does not affect the callbacks for UI related extension methods. You will always get the
|
||||
/// callbacks called from main queue.
|
||||
case callbackQueue(CallbackQueue)
|
||||
|
||||
/// The associated value will be used as the scale factor when converting retrieved data to an image.
|
||||
/// Specify the image scale, instead of your screen scale. You may need to set the correct scale when you dealing
|
||||
/// with 2x or 3x retina images. Otherwise, Kingfisher will convert the data to image object at `scale` 1.0.
|
||||
case scaleFactor(CGFloat)
|
||||
|
||||
/// Whether all the animated image data should be preloaded. Default is `false`, which means only following frames
|
||||
/// will be loaded on need. If `true`, all the animated image data will be loaded and decoded into memory.
|
||||
///
|
||||
/// This option is mainly used for back compatibility internally. You should not set it directly. Instead,
|
||||
/// you should choose the image view class to control the GIF data loading. There are two classes in Kingfisher
|
||||
/// support to display a GIF image. `AnimatedImageView` does not preload all data, it takes much less memory, but
|
||||
/// uses more CPU when display. While a normal image view (`UIImageView` or `NSImageView`) loads all data at once,
|
||||
/// which uses more memory but only decode image frames once.
|
||||
case preloadAllAnimationData
|
||||
|
||||
/// The `ImageDownloadRequestModifier` contained will be used to change the request before it being sent.
|
||||
/// This is the last chance you can modify the image download request. You can modify the request for some
|
||||
/// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping.
|
||||
/// The original request will be sent without any modification by default.
|
||||
case requestModifier(AsyncImageDownloadRequestModifier)
|
||||
|
||||
/// The `ImageDownloadRedirectHandler` contained will be used to change the request before redirection.
|
||||
/// This is the possibility you can modify the image download request during redirect. You can modify the request for
|
||||
/// some customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url
|
||||
/// mapping.
|
||||
/// The original redirection request will be sent without any modification by default.
|
||||
case redirectHandler(ImageDownloadRedirectHandler)
|
||||
|
||||
/// Processor for processing when the downloading finishes, a processor will convert the downloaded data to an image
|
||||
/// and/or apply some filter on it. If a cache is connected to the downloader (it happens when you are using
|
||||
/// KingfisherManager or any of the view extension methods), the converted image will also be sent to cache as well.
|
||||
/// If not set, the `DefaultImageProcessor.default` will be used.
|
||||
case processor(ImageProcessor)
|
||||
|
||||
/// Provides a `CacheSerializer` to convert some data to an image object for
|
||||
/// retrieving from disk cache or vice versa for storing to disk cache.
|
||||
/// If not set, the `DefaultCacheSerializer.default` will be used.
|
||||
case cacheSerializer(CacheSerializer)
|
||||
|
||||
/// An `ImageModifier` is for modifying an image as needed right before it is used. If the image was fetched
|
||||
/// directly from the downloader, the modifier will run directly after the `ImageProcessor`. If the image is being
|
||||
/// fetched from a cache, the modifier will run after the `CacheSerializer`.
|
||||
///
|
||||
/// Use `ImageModifier` when you need to set properties that do not persist when caching the image on a concrete
|
||||
/// type of `Image`, such as the `renderingMode` or the `alignmentInsets` of `UIImage`.
|
||||
case imageModifier(ImageModifier)
|
||||
|
||||
/// Keep the existing image of image view while setting another image to it.
|
||||
/// By setting this option, the placeholder image parameter of image view extension method
|
||||
/// will be ignored and the current image will be kept while loading or downloading the new image.
|
||||
case keepCurrentImageWhileLoading
|
||||
|
||||
/// If set, Kingfisher will only load the first frame from an animated image file as a single image.
|
||||
/// Loading an animated images may take too much memory. It will be useful when you want to display a
|
||||
/// static preview of the first frame from an animated image.
|
||||
///
|
||||
/// This option will be ignored if the target image is not animated image data.
|
||||
case onlyLoadFirstFrame
|
||||
|
||||
/// If set and an `ImageProcessor` is used, Kingfisher will try to cache both the final result and original
|
||||
/// image. Kingfisher will have a chance to use the original image when another processor is applied to the same
|
||||
/// resource, instead of downloading it again. You can use `.originalCache` to specify a cache or the original
|
||||
/// images if necessary.
|
||||
///
|
||||
/// The original image will be only cached to disk storage.
|
||||
case cacheOriginalImage
|
||||
|
||||
/// If set and an image retrieving error occurred Kingfisher will set provided image (or empty)
|
||||
/// in place of requested one. It's useful when you don't want to show placeholder
|
||||
/// during loading time but wants to use some default image when requests will be failed.
|
||||
case onFailureImage(KFCrossPlatformImage?)
|
||||
|
||||
/// If set and used in `ImagePrefetcher`, the prefetching operation will load the images into memory storage
|
||||
/// aggressively. By default this is not contained in the options, that means if the requested image is already
|
||||
/// in disk cache, Kingfisher will not try to load it to memory.
|
||||
case alsoPrefetchToMemory
|
||||
|
||||
/// If set, the disk storage loading will happen in the same calling queue. By default, disk storage file loading
|
||||
/// happens in its own queue with an asynchronous dispatch behavior. Although it provides better non-blocking disk
|
||||
/// loading performance, it also causes a flickering when you reload an image from disk, if the image view already
|
||||
/// has an image set.
|
||||
///
|
||||
/// Set this options will stop that flickering by keeping all loading in the same queue (typically the UI queue
|
||||
/// if you are using Kingfisher's extension methods to set an image), with a tradeoff of loading performance.
|
||||
case loadDiskFileSynchronously
|
||||
|
||||
/// Options to control the writing of data to disk storage
|
||||
/// If set, options will be passed the store operation for a new files.
|
||||
case diskStoreWriteOptions(Data.WritingOptions)
|
||||
|
||||
/// The expiration setting for memory cache. By default, the underlying `MemoryStorage.Backend` uses the
|
||||
/// expiration in its config for all items. If set, the `MemoryStorage.Backend` will use this associated
|
||||
/// value to overwrite the config setting for this caching item.
|
||||
case memoryCacheExpiration(StorageExpiration)
|
||||
|
||||
/// The expiration extending setting for memory cache. The item expiration time will be incremented by this
|
||||
/// value after access.
|
||||
/// By default, the underlying `MemoryStorage.Backend` uses the initial cache expiration as extending
|
||||
/// value: .cacheTime.
|
||||
///
|
||||
/// To disable extending option at all add memoryCacheAccessExtendingExpiration(.none) to options.
|
||||
case memoryCacheAccessExtendingExpiration(ExpirationExtending)
|
||||
|
||||
/// The expiration setting for disk cache. By default, the underlying `DiskStorage.Backend` uses the
|
||||
/// expiration in its config for all items. If set, the `DiskStorage.Backend` will use this associated
|
||||
/// value to overwrite the config setting for this caching item.
|
||||
case diskCacheExpiration(StorageExpiration)
|
||||
|
||||
/// The expiration extending setting for disk cache. The item expiration time will be incremented by this value after access.
|
||||
/// By default, the underlying `DiskStorage.Backend` uses the initial cache expiration as extending value: .cacheTime.
|
||||
/// To disable extending option at all add diskCacheAccessExtendingExpiration(.none) to options.
|
||||
case diskCacheAccessExtendingExpiration(ExpirationExtending)
|
||||
|
||||
/// Decides on which queue the image processing should happen. By default, Kingfisher uses a pre-defined serial
|
||||
/// queue to process images. Use this option to change this behavior. For example, specify a `.mainCurrentOrAsync`
|
||||
/// to let the image be processed in main queue to prevent a possible flickering (but with a possibility of
|
||||
/// blocking the UI, especially if the processor needs a lot of time to run).
|
||||
case processingQueue(CallbackQueue)
|
||||
|
||||
/// Enable progressive image loading, Kingfisher will use the associated `ImageProgressive` value to process the
|
||||
/// progressive JPEG data and display it in a progressive way.
|
||||
case progressiveJPEG(ImageProgressive)
|
||||
|
||||
/// The alternative sources will be used when the original input `Source` fails. The `Source`s in the associated
|
||||
/// array will be used to start a new image loading task if the previous task fails due to an error. The image
|
||||
/// source loading process will stop as soon as a source is loaded successfully. If all `[Source]`s are used but
|
||||
/// the loading is still failing, an `imageSettingError` with `alternativeSourcesExhausted` as its reason will be
|
||||
/// thrown out.
|
||||
///
|
||||
/// This option is useful if you want to implement a fallback solution for setting image.
|
||||
///
|
||||
/// User cancellation will not trigger the alternative source loading.
|
||||
case alternativeSources([Source])
|
||||
|
||||
/// Provide a retry strategy which will be used when something gets wrong during the image retrieving process from
|
||||
/// `KingfisherManager`. You can define a strategy by create a type conforming to the `RetryStrategy` protocol.
|
||||
///
|
||||
/// - Note:
|
||||
///
|
||||
/// All extension methods of Kingfisher (`kf` extensions on `UIImageView` or `UIButton`) retrieve images through
|
||||
/// `KingfisherManager`, so the retry strategy also applies when using them. However, this option does not apply
|
||||
/// when pass to an `ImageDownloader` or `ImageCache`.
|
||||
///
|
||||
case retryStrategy(RetryStrategy)
|
||||
|
||||
/// The `Source` should be loaded when user enables Low Data Mode and the original source fails with an
|
||||
/// `NSURLErrorNetworkUnavailableReason.constrained` error. When this option is set, the
|
||||
/// `allowsConstrainedNetworkAccess` property of the request for the original source will be set to `false` and the
|
||||
/// `Source` in associated value will be used to retrieve the image for low data mode. Usually, you can provide a
|
||||
/// low-resolution version of your image or a local image provider to display a placeholder.
|
||||
///
|
||||
/// If not set or the `source` is `nil`, the device Low Data Mode will be ignored and the original source will
|
||||
/// be loaded following the system default behavior, in a normal way.
|
||||
case lowDataMode(Source?)
|
||||
}
|
||||
|
||||
// Improve performance by parsing the input `KingfisherOptionsInfo` (self) first.
|
||||
// So we can prevent the iterating over the options array again and again.
|
||||
/// The parsed options info used across Kingfisher methods. Each property in this type corresponds a case member
|
||||
/// in `KingfisherOptionsInfoItem`. When a `KingfisherOptionsInfo` sent to Kingfisher related methods, it will be
|
||||
/// parsed and converted to a `KingfisherParsedOptionsInfo` first, and pass through the internal methods.
|
||||
public struct KingfisherParsedOptionsInfo {
|
||||
|
||||
public var targetCache: ImageCache? = nil
|
||||
public var originalCache: ImageCache? = nil
|
||||
public var downloader: ImageDownloader? = nil
|
||||
public var transition: ImageTransition = .none
|
||||
public var downloadPriority: Float = URLSessionTask.defaultPriority
|
||||
public var forceRefresh = false
|
||||
public var fromMemoryCacheOrRefresh = false
|
||||
public var forceTransition = false
|
||||
public var cacheMemoryOnly = false
|
||||
public var waitForCache = false
|
||||
public var onlyFromCache = false
|
||||
public var backgroundDecode = false
|
||||
public var preloadAllAnimationData = false
|
||||
public var callbackQueue: CallbackQueue = .mainCurrentOrAsync
|
||||
public var scaleFactor: CGFloat = 1.0
|
||||
public var requestModifier: AsyncImageDownloadRequestModifier? = nil
|
||||
public var redirectHandler: ImageDownloadRedirectHandler? = nil
|
||||
public var processor: ImageProcessor = DefaultImageProcessor.default
|
||||
public var imageModifier: ImageModifier? = nil
|
||||
public var cacheSerializer: CacheSerializer = DefaultCacheSerializer.default
|
||||
public var keepCurrentImageWhileLoading = false
|
||||
public var onlyLoadFirstFrame = false
|
||||
public var cacheOriginalImage = false
|
||||
public var onFailureImage: Optional<KFCrossPlatformImage?> = .none
|
||||
public var alsoPrefetchToMemory = false
|
||||
public var loadDiskFileSynchronously = false
|
||||
public var diskStoreWriteOptions: Data.WritingOptions = []
|
||||
public var memoryCacheExpiration: StorageExpiration? = nil
|
||||
public var memoryCacheAccessExtendingExpiration: ExpirationExtending = .cacheTime
|
||||
public var diskCacheExpiration: StorageExpiration? = nil
|
||||
public var diskCacheAccessExtendingExpiration: ExpirationExtending = .cacheTime
|
||||
public var processingQueue: CallbackQueue? = nil
|
||||
public var progressiveJPEG: ImageProgressive? = nil
|
||||
public var alternativeSources: [Source]? = nil
|
||||
public var retryStrategy: RetryStrategy? = nil
|
||||
public var lowDataModeSource: Source? = nil
|
||||
|
||||
var onDataReceived: [DataReceivingSideEffect]? = nil
|
||||
|
||||
public init(_ info: KingfisherOptionsInfo?) {
|
||||
guard let info = info else { return }
|
||||
for option in info {
|
||||
switch option {
|
||||
case .targetCache(let value): targetCache = value
|
||||
case .originalCache(let value): originalCache = value
|
||||
case .downloader(let value): downloader = value
|
||||
case .transition(let value): transition = value
|
||||
case .downloadPriority(let value): downloadPriority = value
|
||||
case .forceRefresh: forceRefresh = true
|
||||
case .fromMemoryCacheOrRefresh: fromMemoryCacheOrRefresh = true
|
||||
case .forceTransition: forceTransition = true
|
||||
case .cacheMemoryOnly: cacheMemoryOnly = true
|
||||
case .waitForCache: waitForCache = true
|
||||
case .onlyFromCache: onlyFromCache = true
|
||||
case .backgroundDecode: backgroundDecode = true
|
||||
case .preloadAllAnimationData: preloadAllAnimationData = true
|
||||
case .callbackQueue(let value): callbackQueue = value
|
||||
case .scaleFactor(let value): scaleFactor = value
|
||||
case .requestModifier(let value): requestModifier = value
|
||||
case .redirectHandler(let value): redirectHandler = value
|
||||
case .processor(let value): processor = value
|
||||
case .imageModifier(let value): imageModifier = value
|
||||
case .cacheSerializer(let value): cacheSerializer = value
|
||||
case .keepCurrentImageWhileLoading: keepCurrentImageWhileLoading = true
|
||||
case .onlyLoadFirstFrame: onlyLoadFirstFrame = true
|
||||
case .cacheOriginalImage: cacheOriginalImage = true
|
||||
case .onFailureImage(let value): onFailureImage = .some(value)
|
||||
case .alsoPrefetchToMemory: alsoPrefetchToMemory = true
|
||||
case .loadDiskFileSynchronously: loadDiskFileSynchronously = true
|
||||
case .diskStoreWriteOptions(let options): diskStoreWriteOptions = options
|
||||
case .memoryCacheExpiration(let expiration): memoryCacheExpiration = expiration
|
||||
case .memoryCacheAccessExtendingExpiration(let expirationExtending): memoryCacheAccessExtendingExpiration = expirationExtending
|
||||
case .diskCacheExpiration(let expiration): diskCacheExpiration = expiration
|
||||
case .diskCacheAccessExtendingExpiration(let expirationExtending): diskCacheAccessExtendingExpiration = expirationExtending
|
||||
case .processingQueue(let queue): processingQueue = queue
|
||||
case .progressiveJPEG(let value): progressiveJPEG = value
|
||||
case .alternativeSources(let sources): alternativeSources = sources
|
||||
case .retryStrategy(let strategy): retryStrategy = strategy
|
||||
case .lowDataMode(let source): lowDataModeSource = source
|
||||
}
|
||||
}
|
||||
|
||||
if originalCache == nil {
|
||||
originalCache = targetCache
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension KingfisherParsedOptionsInfo {
|
||||
var imageCreatingOptions: ImageCreatingOptions {
|
||||
return ImageCreatingOptions(
|
||||
scale: scaleFactor,
|
||||
duration: 0.0,
|
||||
preloadAll: preloadAllAnimationData,
|
||||
onlyFirstFrame: onlyLoadFirstFrame)
|
||||
}
|
||||
}
|
||||
|
||||
protocol DataReceivingSideEffect: AnyObject {
|
||||
var onShouldApply: () -> Bool { get set }
|
||||
func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data)
|
||||
}
|
||||
|
||||
class ImageLoadingProgressSideEffect: DataReceivingSideEffect {
|
||||
|
||||
var onShouldApply: () -> Bool = { return true }
|
||||
|
||||
let block: DownloadProgressBlock
|
||||
|
||||
init(_ block: @escaping DownloadProgressBlock) {
|
||||
self.block = block
|
||||
}
|
||||
|
||||
func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) {
|
||||
guard self.onShouldApply() else { return }
|
||||
guard let expectedContentLength = task.task.response?.expectedContentLength,
|
||||
expectedContentLength != -1 else
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
let dataLength = Int64(task.mutableData.count)
|
||||
DispatchQueue.main.async {
|
||||
self.block(dataLength, expectedContentLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
146
Pods/Kingfisher/Sources/Image/Filter.swift
generated
Normal file
146
Pods/Kingfisher/Sources/Image/Filter.swift
generated
Normal file
@@ -0,0 +1,146 @@
|
||||
//
|
||||
// Filter.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 2016/08/31.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
import CoreImage
|
||||
|
||||
// Reuse the same CI Context for all CI drawing.
|
||||
private let ciContext = CIContext(options: nil)
|
||||
|
||||
/// Represents the type of transformer method, which will be used in to provide a `Filter`.
|
||||
public typealias Transformer = (CIImage) -> CIImage?
|
||||
|
||||
/// Represents a processor based on a `CIImage` `Filter`.
|
||||
/// It requires a filter to create an `ImageProcessor`.
|
||||
public protocol CIImageProcessor: ImageProcessor {
|
||||
var filter: Filter { get }
|
||||
}
|
||||
|
||||
extension CIImageProcessor {
|
||||
|
||||
/// Processes the input `ImageProcessItem` with this processor.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - item: Input item which will be processed by `self`.
|
||||
/// - options: Options when processing the item.
|
||||
/// - Returns: The processed image.
|
||||
///
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
switch item {
|
||||
case .image(let image):
|
||||
return image.kf.apply(filter)
|
||||
case .data:
|
||||
return (DefaultImageProcessor.default |> self).process(item: item, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper struct for a `Transformer` of CIImage filters. A `Filter`
|
||||
/// value could be used to create a `CIImage` processor.
|
||||
public struct Filter {
|
||||
|
||||
let transform: Transformer
|
||||
|
||||
public init(transform: @escaping Transformer) {
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
/// Tint filter which will apply a tint color to images.
|
||||
public static var tint: (KFCrossPlatformColor) -> Filter = {
|
||||
color in
|
||||
Filter {
|
||||
input in
|
||||
|
||||
let colorFilter = CIFilter(name: "CIConstantColorGenerator")!
|
||||
colorFilter.setValue(CIColor(color: color), forKey: kCIInputColorKey)
|
||||
|
||||
let filter = CIFilter(name: "CISourceOverCompositing")!
|
||||
|
||||
let colorImage = colorFilter.outputImage
|
||||
filter.setValue(colorImage, forKey: kCIInputImageKey)
|
||||
filter.setValue(input, forKey: kCIInputBackgroundImageKey)
|
||||
|
||||
return filter.outputImage?.cropped(to: input.extent)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents color control elements. It is a tuple of
|
||||
/// `(brightness, contrast, saturation, inputEV)`
|
||||
public typealias ColorElement = (CGFloat, CGFloat, CGFloat, CGFloat)
|
||||
|
||||
/// Color control filter which will apply color control change to images.
|
||||
public static var colorControl: (ColorElement) -> Filter = { arg -> Filter in
|
||||
let (brightness, contrast, saturation, inputEV) = arg
|
||||
return Filter { input in
|
||||
let paramsColor = [kCIInputBrightnessKey: brightness,
|
||||
kCIInputContrastKey: contrast,
|
||||
kCIInputSaturationKey: saturation]
|
||||
let blackAndWhite = input.applyingFilter("CIColorControls", parameters: paramsColor)
|
||||
let paramsExposure = [kCIInputEVKey: inputEV]
|
||||
return blackAndWhite.applyingFilter("CIExposureAdjust", parameters: paramsExposure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension KingfisherWrapper where Base: KFCrossPlatformImage {
|
||||
|
||||
/// Applies a `Filter` containing `CIImage` transformer to `self`.
|
||||
///
|
||||
/// - Parameter filter: The filter used to transform `self`.
|
||||
/// - Returns: A transformed image by input `Filter`.
|
||||
///
|
||||
/// - Note:
|
||||
/// Only CG-based images are supported. If any error happens
|
||||
/// during transforming, `self` will be returned.
|
||||
public func apply(_ filter: Filter) -> KFCrossPlatformImage {
|
||||
|
||||
guard let cgImage = cgImage else {
|
||||
assertionFailure("[Kingfisher] Tint image only works for CG-based image.")
|
||||
return base
|
||||
}
|
||||
|
||||
let inputImage = CIImage(cgImage: cgImage)
|
||||
guard let outputImage = filter.transform(inputImage) else {
|
||||
return base
|
||||
}
|
||||
|
||||
guard let result = ciContext.createCGImage(outputImage, from: outputImage.extent) else {
|
||||
assertionFailure("[Kingfisher] Can not make an tint image within context.")
|
||||
return base
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
return fixedForRetinaPixel(cgImage: result, to: size)
|
||||
#else
|
||||
return KFCrossPlatformImage(cgImage: result, scale: base.scale, orientation: base.imageOrientation)
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
177
Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift
generated
Normal file
177
Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift
generated
Normal file
@@ -0,0 +1,177 @@
|
||||
//
|
||||
// AnimatedImage.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2018/09/26.
|
||||
//
|
||||
// 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
|
||||
import ImageIO
|
||||
|
||||
/// Represents a set of image creating options used in Kingfisher.
|
||||
public struct ImageCreatingOptions {
|
||||
|
||||
/// The target scale of image needs to be created.
|
||||
public let scale: CGFloat
|
||||
|
||||
/// The expected animation duration if an animated image being created.
|
||||
public let duration: TimeInterval
|
||||
|
||||
/// For an animated image, whether or not all frames should be loaded before displaying.
|
||||
public let preloadAll: Bool
|
||||
|
||||
/// For an animated image, whether or not only the first image should be
|
||||
/// loaded as a static image. It is useful for preview purpose of an animated image.
|
||||
public let onlyFirstFrame: Bool
|
||||
|
||||
/// Creates an `ImageCreatingOptions` object.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - scale: The target scale of image needs to be created. Default is `1.0`.
|
||||
/// - duration: The expected animation duration if an animated image being created.
|
||||
/// A value less or equal to `0.0` means the animated image duration will
|
||||
/// be determined by the frame data. Default is `0.0`.
|
||||
/// - preloadAll: For an animated image, whether or not all frames should be loaded before displaying.
|
||||
/// Default is `false`.
|
||||
/// - onlyFirstFrame: For an animated image, whether or not only the first image should be
|
||||
/// loaded as a static image. It is useful for preview purpose of an animated image.
|
||||
/// Default is `false`.
|
||||
public init(
|
||||
scale: CGFloat = 1.0,
|
||||
duration: TimeInterval = 0.0,
|
||||
preloadAll: Bool = false,
|
||||
onlyFirstFrame: Bool = false)
|
||||
{
|
||||
self.scale = scale
|
||||
self.duration = duration
|
||||
self.preloadAll = preloadAll
|
||||
self.onlyFirstFrame = onlyFirstFrame
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the decoding for a GIF image. This class extracts frames from an `imageSource`, then
|
||||
/// hold the images for later use.
|
||||
public class GIFAnimatedImage {
|
||||
let images: [KFCrossPlatformImage]
|
||||
let duration: TimeInterval
|
||||
|
||||
init?(from frameSource: ImageFrameSource, options: ImageCreatingOptions) {
|
||||
let frameCount = frameSource.frameCount
|
||||
var images = [KFCrossPlatformImage]()
|
||||
var gifDuration = 0.0
|
||||
|
||||
for i in 0 ..< frameCount {
|
||||
guard let imageRef = frameSource.frame(at: i) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if frameCount == 1 {
|
||||
gifDuration = .infinity
|
||||
} else {
|
||||
// Get current animated GIF frame duration
|
||||
gifDuration += frameSource.duration(at: i)
|
||||
}
|
||||
images.append(KingfisherWrapper.image(cgImage: imageRef, scale: options.scale, refImage: nil))
|
||||
if options.onlyFirstFrame { break }
|
||||
}
|
||||
self.images = images
|
||||
self.duration = gifDuration
|
||||
}
|
||||
|
||||
convenience init?(from imageSource: CGImageSource, for info: [String: Any], options: ImageCreatingOptions) {
|
||||
let frameSource = CGImageFrameSource(data: nil, imageSource: imageSource, options: info)
|
||||
self.init(from: frameSource, options: options)
|
||||
}
|
||||
|
||||
/// Calculates frame duration for a gif frame out of the kCGImagePropertyGIFDictionary dictionary.
|
||||
public static func getFrameDuration(from gifInfo: [String: Any]?) -> TimeInterval {
|
||||
let defaultFrameDuration = 0.1
|
||||
guard let gifInfo = gifInfo else { return defaultFrameDuration }
|
||||
|
||||
let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber
|
||||
let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber
|
||||
let duration = unclampedDelayTime ?? delayTime
|
||||
|
||||
guard let frameDuration = duration else { return defaultFrameDuration }
|
||||
return frameDuration.doubleValue > 0.011 ? frameDuration.doubleValue : defaultFrameDuration
|
||||
}
|
||||
|
||||
/// Calculates frame duration at a specific index for a gif from an `imageSource`.
|
||||
public static func getFrameDuration(from imageSource: CGImageSource, at index: Int) -> TimeInterval {
|
||||
guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, index, nil)
|
||||
as? [String: Any] else { return 0.0 }
|
||||
|
||||
let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any]
|
||||
return getFrameDuration(from: gifInfo)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a frame source for animated image
|
||||
public protocol ImageFrameSource {
|
||||
/// Source data associated with this frame source.
|
||||
var data: Data? { get }
|
||||
|
||||
/// Count of total frames in this frame source.
|
||||
var frameCount: Int { get }
|
||||
|
||||
/// Retrieves the frame at a specific index. The result image is expected to be
|
||||
/// no larger than `maxSize`. If the index is invalid, implementors should return `nil`.
|
||||
func frame(at index: Int, maxSize: CGSize?) -> CGImage?
|
||||
|
||||
/// Retrieves the duration at a specific index. If the index is invalid, implementors should return `0.0`.
|
||||
func duration(at index: Int) -> TimeInterval
|
||||
}
|
||||
|
||||
public extension ImageFrameSource {
|
||||
/// Retrieves the frame at a specific index. If the index is invalid, implementors should return `nil`.
|
||||
func frame(at index: Int) -> CGImage? {
|
||||
return frame(at: index, maxSize: nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct CGImageFrameSource: ImageFrameSource {
|
||||
let data: Data?
|
||||
let imageSource: CGImageSource
|
||||
let options: [String: Any]?
|
||||
|
||||
var frameCount: Int {
|
||||
return CGImageSourceGetCount(imageSource)
|
||||
}
|
||||
|
||||
func frame(at index: Int, maxSize: CGSize?) -> CGImage? {
|
||||
var options = self.options as? [CFString: Any]
|
||||
if let maxSize = maxSize, maxSize != .zero {
|
||||
options = (options ?? [:]).merging([
|
||||
kCGImageSourceCreateThumbnailFromImageIfAbsent: true,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||
kCGImageSourceShouldCacheImmediately: true,
|
||||
kCGImageSourceThumbnailMaxPixelSize: max(maxSize.width, maxSize.height)
|
||||
], uniquingKeysWith: { $1 })
|
||||
}
|
||||
return CGImageSourceCreateImageAtIndex(imageSource, index, options as CFDictionary?)
|
||||
}
|
||||
|
||||
func duration(at index: Int) -> TimeInterval {
|
||||
return GIFAnimatedImage.getFrameDuration(from: imageSource, at: index)
|
||||
}
|
||||
}
|
||||
|
||||
88
Pods/Kingfisher/Sources/Image/GraphicsContext.swift
generated
Normal file
88
Pods/Kingfisher/Sources/Image/GraphicsContext.swift
generated
Normal file
@@ -0,0 +1,88 @@
|
||||
//
|
||||
// GraphicsContext.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by taras on 19/04/2021.
|
||||
//
|
||||
// Copyright (c) 2021 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.
|
||||
|
||||
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
|
||||
import AppKit
|
||||
#endif
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
enum GraphicsContext {
|
||||
static func begin(size: CGSize, scale: CGFloat) {
|
||||
#if os(macOS)
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
#else
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, scale)
|
||||
#endif
|
||||
}
|
||||
|
||||
static func current(size: CGSize, scale: CGFloat, inverting: Bool, cgImage: CGImage?) -> CGContext? {
|
||||
#if os(macOS)
|
||||
guard let rep = NSBitmapImageRep(
|
||||
bitmapDataPlanes: nil,
|
||||
pixelsWide: Int(size.width),
|
||||
pixelsHigh: Int(size.height),
|
||||
bitsPerSample: cgImage?.bitsPerComponent ?? 8,
|
||||
samplesPerPixel: 4,
|
||||
hasAlpha: true,
|
||||
isPlanar: false,
|
||||
colorSpaceName: .calibratedRGB,
|
||||
bytesPerRow: 0,
|
||||
bitsPerPixel: 0) else
|
||||
{
|
||||
assertionFailure("[Kingfisher] Image representation cannot be created.")
|
||||
return nil
|
||||
}
|
||||
rep.size = size
|
||||
guard let context = NSGraphicsContext(bitmapImageRep: rep) else {
|
||||
assertionFailure("[Kingfisher] Image context cannot be created.")
|
||||
return nil
|
||||
}
|
||||
|
||||
NSGraphicsContext.current = context
|
||||
return context.cgContext
|
||||
#else
|
||||
guard let context = UIGraphicsGetCurrentContext() else {
|
||||
return nil
|
||||
}
|
||||
if inverting { // If drawing a CGImage, we need to make context flipped.
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.translateBy(x: 0, y: -size.height)
|
||||
}
|
||||
return context
|
||||
#endif
|
||||
}
|
||||
|
||||
static func end() {
|
||||
#if os(macOS)
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
#else
|
||||
UIGraphicsEndImageContext()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
426
Pods/Kingfisher/Sources/Image/Image.swift
generated
Normal file
426
Pods/Kingfisher/Sources/Image/Image.swift
generated
Normal file
@@ -0,0 +1,426 @@
|
||||
//
|
||||
// Image.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 16/1/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.
|
||||
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
private var imagesKey: Void?
|
||||
private var durationKey: Void?
|
||||
#else
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
private var imageSourceKey: Void?
|
||||
#endif
|
||||
|
||||
#if !os(watchOS)
|
||||
import CoreImage
|
||||
#endif
|
||||
|
||||
import CoreGraphics
|
||||
import ImageIO
|
||||
|
||||
#if canImport(UniformTypeIdentifiers)
|
||||
import UniformTypeIdentifiers
|
||||
#endif
|
||||
|
||||
private var animatedImageDataKey: Void?
|
||||
private var imageFrameCountKey: Void?
|
||||
|
||||
// MARK: - Image Properties
|
||||
extension KingfisherWrapper where Base: KFCrossPlatformImage {
|
||||
private(set) var animatedImageData: Data? {
|
||||
get { return getAssociatedObject(base, &animatedImageDataKey) }
|
||||
set { setRetainedAssociatedObject(base, &animatedImageDataKey, newValue) }
|
||||
}
|
||||
|
||||
public var imageFrameCount: Int? {
|
||||
get { return getAssociatedObject(base, &imageFrameCountKey) }
|
||||
set { setRetainedAssociatedObject(base, &imageFrameCountKey, newValue) }
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
var cgImage: CGImage? {
|
||||
return base.cgImage(forProposedRect: nil, context: nil, hints: nil)
|
||||
}
|
||||
|
||||
var scale: CGFloat {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
private(set) var images: [KFCrossPlatformImage]? {
|
||||
get { return getAssociatedObject(base, &imagesKey) }
|
||||
set { setRetainedAssociatedObject(base, &imagesKey, newValue) }
|
||||
}
|
||||
|
||||
private(set) var duration: TimeInterval {
|
||||
get { return getAssociatedObject(base, &durationKey) ?? 0.0 }
|
||||
set { setRetainedAssociatedObject(base, &durationKey, newValue) }
|
||||
}
|
||||
|
||||
var size: CGSize {
|
||||
return base.representations.reduce(.zero) { size, rep in
|
||||
let width = max(size.width, CGFloat(rep.pixelsWide))
|
||||
let height = max(size.height, CGFloat(rep.pixelsHigh))
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
}
|
||||
#else
|
||||
var cgImage: CGImage? { return base.cgImage }
|
||||
var scale: CGFloat { return base.scale }
|
||||
var images: [KFCrossPlatformImage]? { return base.images }
|
||||
var duration: TimeInterval { return base.duration }
|
||||
var size: CGSize { return base.size }
|
||||
|
||||
/// The image source reference of current image.
|
||||
public var imageSource: CGImageSource? {
|
||||
get {
|
||||
guard let frameSource = frameSource as? CGImageFrameSource else { return nil }
|
||||
return frameSource.imageSource
|
||||
}
|
||||
}
|
||||
|
||||
/// The custom frame source of current image.
|
||||
public private(set) var frameSource: ImageFrameSource? {
|
||||
get { return getAssociatedObject(base, &imageSourceKey) }
|
||||
set { setRetainedAssociatedObject(base, &imageSourceKey, newValue) }
|
||||
}
|
||||
#endif
|
||||
|
||||
// Bitmap memory cost with bytes.
|
||||
var cost: Int {
|
||||
let pixel = Int(size.width * size.height * scale * scale)
|
||||
guard let cgImage = cgImage else {
|
||||
return pixel * 4
|
||||
}
|
||||
let bytesPerPixel = cgImage.bitsPerPixel / 8
|
||||
guard let imageCount = images?.count else {
|
||||
return pixel * bytesPerPixel
|
||||
}
|
||||
return pixel * bytesPerPixel * imageCount
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image Conversion
|
||||
extension KingfisherWrapper where Base: KFCrossPlatformImage {
|
||||
#if os(macOS)
|
||||
static func image(cgImage: CGImage, scale: CGFloat, refImage: KFCrossPlatformImage?) -> KFCrossPlatformImage {
|
||||
return KFCrossPlatformImage(cgImage: cgImage, size: .zero)
|
||||
}
|
||||
|
||||
/// Normalize the image. This getter does nothing on macOS but return the image itself.
|
||||
public var normalized: KFCrossPlatformImage { return base }
|
||||
|
||||
#else
|
||||
/// Creating an image from a give `CGImage` at scale and orientation for refImage. The method signature is for
|
||||
/// compatibility of macOS version.
|
||||
static func image(cgImage: CGImage, scale: CGFloat, refImage: KFCrossPlatformImage?) -> KFCrossPlatformImage {
|
||||
return KFCrossPlatformImage(cgImage: cgImage, scale: scale, orientation: refImage?.imageOrientation ?? .up)
|
||||
}
|
||||
|
||||
/// Returns normalized image for current `base` image.
|
||||
/// This method will try to redraw an image with orientation and scale considered.
|
||||
public var normalized: KFCrossPlatformImage {
|
||||
// prevent animated image (GIF) lose it's images
|
||||
guard images == nil else { return base.copy() as! KFCrossPlatformImage }
|
||||
// No need to do anything if already up
|
||||
guard base.imageOrientation != .up else { return base.copy() as! KFCrossPlatformImage }
|
||||
|
||||
return draw(to: size, inverting: true, refImage: KFCrossPlatformImage()) {
|
||||
fixOrientation(in: $0)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func fixOrientation(in context: CGContext) {
|
||||
|
||||
var transform = CGAffineTransform.identity
|
||||
|
||||
let orientation = base.imageOrientation
|
||||
|
||||
switch orientation {
|
||||
case .down, .downMirrored:
|
||||
transform = transform.translatedBy(x: size.width, y: size.height)
|
||||
transform = transform.rotated(by: .pi)
|
||||
case .left, .leftMirrored:
|
||||
transform = transform.translatedBy(x: size.width, y: 0)
|
||||
transform = transform.rotated(by: .pi / 2.0)
|
||||
case .right, .rightMirrored:
|
||||
transform = transform.translatedBy(x: 0, y: size.height)
|
||||
transform = transform.rotated(by: .pi / -2.0)
|
||||
case .up, .upMirrored:
|
||||
break
|
||||
#if compiler(>=5)
|
||||
@unknown default:
|
||||
break
|
||||
#endif
|
||||
}
|
||||
|
||||
//Flip image one more time if needed to, this is to prevent flipped image
|
||||
switch orientation {
|
||||
case .upMirrored, .downMirrored:
|
||||
transform = transform.translatedBy(x: size.width, y: 0)
|
||||
transform = transform.scaledBy(x: -1, y: 1)
|
||||
case .leftMirrored, .rightMirrored:
|
||||
transform = transform.translatedBy(x: size.height, y: 0)
|
||||
transform = transform.scaledBy(x: -1, y: 1)
|
||||
case .up, .down, .left, .right:
|
||||
break
|
||||
#if compiler(>=5)
|
||||
@unknown default:
|
||||
break
|
||||
#endif
|
||||
}
|
||||
|
||||
context.concatenate(transform)
|
||||
switch orientation {
|
||||
case .left, .leftMirrored, .right, .rightMirrored:
|
||||
context.draw(cgImage!, in: CGRect(x: 0, y: 0, width: size.height, height: size.width))
|
||||
default:
|
||||
context.draw(cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - Image Representation
|
||||
extension KingfisherWrapper where Base: KFCrossPlatformImage {
|
||||
/// Returns PNG representation of `base` image.
|
||||
///
|
||||
/// - Returns: PNG data of image.
|
||||
public func pngRepresentation() -> Data? {
|
||||
#if os(macOS)
|
||||
guard let cgImage = cgImage else {
|
||||
return nil
|
||||
}
|
||||
let rep = NSBitmapImageRep(cgImage: cgImage)
|
||||
return rep.representation(using: .png, properties: [:])
|
||||
#else
|
||||
return base.pngData()
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Returns JPEG representation of `base` image.
|
||||
///
|
||||
/// - Parameter compressionQuality: The compression quality when converting image to JPEG data.
|
||||
/// - Returns: JPEG data of image.
|
||||
public func jpegRepresentation(compressionQuality: CGFloat) -> Data? {
|
||||
#if os(macOS)
|
||||
guard let cgImage = cgImage else {
|
||||
return nil
|
||||
}
|
||||
let rep = NSBitmapImageRep(cgImage: cgImage)
|
||||
return rep.representation(using:.jpeg, properties: [.compressionFactor: compressionQuality])
|
||||
#else
|
||||
return base.jpegData(compressionQuality: compressionQuality)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Returns GIF representation of `base` image.
|
||||
///
|
||||
/// - Returns: Original GIF data of image.
|
||||
public func gifRepresentation() -> Data? {
|
||||
return animatedImageData
|
||||
}
|
||||
|
||||
/// Returns a data representation for `base` image, with the `format` as the format indicator.
|
||||
/// - Parameters:
|
||||
/// - format: The format in which the output data should be. If `unknown`, the `base` image will be
|
||||
/// converted in the PNG representation.
|
||||
/// - compressionQuality: The compression quality when converting image to a lossy format data.
|
||||
///
|
||||
/// - Returns: The output data representing.
|
||||
public func data(format: ImageFormat, compressionQuality: CGFloat = 1.0) -> Data? {
|
||||
return autoreleasepool { () -> Data? in
|
||||
let data: Data?
|
||||
switch format {
|
||||
case .PNG: data = pngRepresentation()
|
||||
case .JPEG: data = jpegRepresentation(compressionQuality: compressionQuality)
|
||||
case .GIF: data = gifRepresentation()
|
||||
case .unknown: data = normalized.kf.pngRepresentation()
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Creating Images
|
||||
extension KingfisherWrapper where Base: KFCrossPlatformImage {
|
||||
|
||||
/// Creates an animated image from a given data and options. Currently only GIF data is supported.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - data: The animated image data.
|
||||
/// - options: Options to use when creating the animated image.
|
||||
/// - Returns: An `Image` object represents the animated image. It is in form of an array of image frames with a
|
||||
/// certain duration. `nil` if anything wrong when creating animated image.
|
||||
public static func animatedImage(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? {
|
||||
#if os(xrOS)
|
||||
let info: [String: Any] = [
|
||||
kCGImageSourceShouldCache as String: true,
|
||||
kCGImageSourceTypeIdentifierHint as String: UTType.gif.identifier
|
||||
]
|
||||
#else
|
||||
let info: [String: Any] = [
|
||||
kCGImageSourceShouldCache as String: true,
|
||||
kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF
|
||||
]
|
||||
#endif
|
||||
|
||||
guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else {
|
||||
return nil
|
||||
}
|
||||
let frameSource = CGImageFrameSource(data: data, imageSource: imageSource, options: info)
|
||||
#if os(macOS)
|
||||
let baseImage = KFCrossPlatformImage(data: data)
|
||||
#else
|
||||
let baseImage = KFCrossPlatformImage(data: data, scale: options.scale)
|
||||
#endif
|
||||
return animatedImage(source: frameSource, options: options, baseImage: baseImage)
|
||||
}
|
||||
|
||||
/// Creates an animated image from a given frame source.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - source: The frame source to create animated image from.
|
||||
/// - options: Options to use when creating the animated image.
|
||||
/// - baseImage: An optional image object to be used as the key frame of the animated image. If `nil`, the first
|
||||
/// frame of the `source` will be used.
|
||||
/// - Returns: An `Image` object represents the animated image. It is in form of an array of image frames with a
|
||||
/// certain duration. `nil` if anything wrong when creating animated image.
|
||||
public static func animatedImage(source: ImageFrameSource, options: ImageCreatingOptions, baseImage: KFCrossPlatformImage? = nil) -> KFCrossPlatformImage? {
|
||||
#if os(macOS)
|
||||
guard let animatedImage = GIFAnimatedImage(from: source, options: options) else {
|
||||
return nil
|
||||
}
|
||||
var image: KFCrossPlatformImage?
|
||||
if options.onlyFirstFrame {
|
||||
image = animatedImage.images.first
|
||||
} else {
|
||||
if let baseImage = baseImage {
|
||||
image = baseImage
|
||||
} else {
|
||||
image = animatedImage.images.first
|
||||
}
|
||||
var kf = image?.kf
|
||||
kf?.images = animatedImage.images
|
||||
kf?.duration = animatedImage.duration
|
||||
}
|
||||
image?.kf.animatedImageData = source.data
|
||||
image?.kf.imageFrameCount = source.frameCount
|
||||
return image
|
||||
#else
|
||||
|
||||
var image: KFCrossPlatformImage?
|
||||
if options.preloadAll || options.onlyFirstFrame {
|
||||
// Use `images` image if you want to preload all animated data
|
||||
guard let animatedImage = GIFAnimatedImage(from: source, options: options) else {
|
||||
return nil
|
||||
}
|
||||
if options.onlyFirstFrame {
|
||||
image = animatedImage.images.first
|
||||
} else {
|
||||
let duration = options.duration <= 0.0 ? animatedImage.duration : options.duration
|
||||
image = .animatedImage(with: animatedImage.images, duration: duration)
|
||||
}
|
||||
image?.kf.animatedImageData = source.data
|
||||
} else {
|
||||
if let baseImage = baseImage {
|
||||
image = baseImage
|
||||
} else {
|
||||
guard let firstFrame = source.frame(at: 0) else {
|
||||
return nil
|
||||
}
|
||||
image = KFCrossPlatformImage(cgImage: firstFrame, scale: options.scale, orientation: .up)
|
||||
}
|
||||
var kf = image?.kf
|
||||
kf?.frameSource = source
|
||||
kf?.animatedImageData = source.data
|
||||
}
|
||||
|
||||
image?.kf.imageFrameCount = source.frameCount
|
||||
return image
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Creates an image from a given data and options. `.JPEG`, `.PNG` or `.GIF` is supported. For other
|
||||
/// image format, image initializer from system will be used. If no image object could be created from
|
||||
/// the given `data`, `nil` will be returned.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - data: The image data representation.
|
||||
/// - options: Options to use when creating the image.
|
||||
/// - Returns: An `Image` object represents the image if created. If the `data` is invalid or not supported, `nil`
|
||||
/// will be returned.
|
||||
public static func image(data: Data, options: ImageCreatingOptions) -> KFCrossPlatformImage? {
|
||||
var image: KFCrossPlatformImage?
|
||||
switch data.kf.imageFormat {
|
||||
case .JPEG:
|
||||
image = KFCrossPlatformImage(data: data, scale: options.scale)
|
||||
case .PNG:
|
||||
image = KFCrossPlatformImage(data: data, scale: options.scale)
|
||||
case .GIF:
|
||||
image = KingfisherWrapper.animatedImage(data: data, options: options)
|
||||
case .unknown:
|
||||
image = KFCrossPlatformImage(data: data, scale: options.scale)
|
||||
}
|
||||
return image
|
||||
}
|
||||
|
||||
/// Creates a downsampled image from given data to a certain size and scale.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - data: The image data contains a JPEG or PNG image.
|
||||
/// - pointSize: The target size in point to which the image should be downsampled.
|
||||
/// - scale: The scale of result image.
|
||||
/// - Returns: A downsampled `Image` object following the input conditions.
|
||||
///
|
||||
/// - Note:
|
||||
/// Different from image `resize` methods, downsampling will not render the original
|
||||
/// input image in pixel format. It does downsampling from the image data, so it is much
|
||||
/// more memory efficient and friendly. Choose to use downsampling as possible as you can.
|
||||
///
|
||||
/// The pointsize should be smaller than the size of input image. If it is larger than the
|
||||
/// original image size, the result image will be the same size of input without downsampling.
|
||||
public static func downsampledImage(data: Data, to pointSize: CGSize, scale: CGFloat) -> KFCrossPlatformImage? {
|
||||
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
|
||||
guard let imageSource = CGImageSourceCreateWithData(data as CFData, imageSourceOptions) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
|
||||
let downsampleOptions: [CFString : Any] = [
|
||||
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
||||
kCGImageSourceShouldCacheImmediately: true,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
|
||||
]
|
||||
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions as CFDictionary) else {
|
||||
return nil
|
||||
}
|
||||
return KingfisherWrapper.image(cgImage: downsampledImage, scale: scale, refImage: nil)
|
||||
}
|
||||
}
|
||||
636
Pods/Kingfisher/Sources/Image/ImageDrawing.swift
generated
Normal file
636
Pods/Kingfisher/Sources/Image/ImageDrawing.swift
generated
Normal file
@@ -0,0 +1,636 @@
|
||||
//
|
||||
// ImageDrawing.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2018/09/28.
|
||||
//
|
||||
// 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 Accelerate
|
||||
|
||||
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
|
||||
import AppKit
|
||||
#endif
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
// MARK: - Image Transforming
|
||||
extension KingfisherWrapper where Base: KFCrossPlatformImage {
|
||||
// MARK: Blend Mode
|
||||
/// Create image from `base` image and apply blend mode.
|
||||
///
|
||||
/// - parameter blendMode: The blend mode of creating image.
|
||||
/// - parameter alpha: The alpha should be used for image.
|
||||
/// - parameter backgroundColor: The background color for the output image.
|
||||
///
|
||||
/// - returns: An image with blend mode applied.
|
||||
///
|
||||
/// - Note: This method only works for CG-based image.
|
||||
#if !os(macOS)
|
||||
public func image(withBlendMode blendMode: CGBlendMode,
|
||||
alpha: CGFloat = 1.0,
|
||||
backgroundColor: KFCrossPlatformColor? = nil) -> KFCrossPlatformImage
|
||||
{
|
||||
guard let _ = cgImage else {
|
||||
assertionFailure("[Kingfisher] Blend mode image only works for CG-based image.")
|
||||
return base
|
||||
}
|
||||
|
||||
let rect = CGRect(origin: .zero, size: size)
|
||||
return draw(to: rect.size, inverting: false) { _ in
|
||||
if let backgroundColor = backgroundColor {
|
||||
backgroundColor.setFill()
|
||||
UIRectFill(rect)
|
||||
}
|
||||
|
||||
base.draw(in: rect, blendMode: blendMode, alpha: alpha)
|
||||
return false
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
// MARK: Compositing
|
||||
/// Creates image from `base` image and apply compositing operation.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - compositingOperation: The compositing operation of creating image.
|
||||
/// - alpha: The alpha should be used for image.
|
||||
/// - backgroundColor: The background color for the output image.
|
||||
/// - Returns: An image with compositing operation applied.
|
||||
///
|
||||
/// - Note: This method only works for CG-based image. For any non-CG-based image, `base` itself is returned.
|
||||
public func image(withCompositingOperation compositingOperation: NSCompositingOperation,
|
||||
alpha: CGFloat = 1.0,
|
||||
backgroundColor: KFCrossPlatformColor? = nil) -> KFCrossPlatformImage
|
||||
{
|
||||
guard let _ = cgImage else {
|
||||
assertionFailure("[Kingfisher] Compositing Operation image only works for CG-based image.")
|
||||
return base
|
||||
}
|
||||
|
||||
let rect = CGRect(origin: .zero, size: size)
|
||||
return draw(to: rect.size, inverting: false) { _ in
|
||||
if let backgroundColor = backgroundColor {
|
||||
backgroundColor.setFill()
|
||||
rect.fill()
|
||||
}
|
||||
base.draw(in: rect, from: .zero, operation: compositingOperation, fraction: alpha)
|
||||
return false
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: Round Corner
|
||||
|
||||
/// Creates a round corner image from on `base` image.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - radius: The round corner radius of creating image.
|
||||
/// - size: The target size of creating image.
|
||||
/// - corners: The target corners which will be applied rounding.
|
||||
/// - backgroundColor: The background color for the output image
|
||||
/// - Returns: An image with round corner of `self`.
|
||||
///
|
||||
/// - Note: This method only works for CG-based image. The current image scale is kept.
|
||||
/// For any non-CG-based image, `base` itself is returned.
|
||||
public func image(
|
||||
withRadius radius: Radius,
|
||||
fit size: CGSize,
|
||||
roundingCorners corners: RectCorner = .all,
|
||||
backgroundColor: KFCrossPlatformColor? = nil
|
||||
) -> KFCrossPlatformImage
|
||||
{
|
||||
|
||||
guard let _ = cgImage else {
|
||||
assertionFailure("[Kingfisher] Round corner image only works for CG-based image.")
|
||||
return base
|
||||
}
|
||||
|
||||
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size)
|
||||
return draw(to: size, inverting: false) { _ in
|
||||
#if os(macOS)
|
||||
if let backgroundColor = backgroundColor {
|
||||
let rectPath = NSBezierPath(rect: rect)
|
||||
backgroundColor.setFill()
|
||||
rectPath.fill()
|
||||
}
|
||||
|
||||
let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners)
|
||||
path.addClip()
|
||||
base.draw(in: rect)
|
||||
#else
|
||||
guard let context = UIGraphicsGetCurrentContext() else {
|
||||
assertionFailure("[Kingfisher] Failed to create CG context for image.")
|
||||
return false
|
||||
}
|
||||
|
||||
if let backgroundColor = backgroundColor {
|
||||
let rectPath = UIBezierPath(rect: rect)
|
||||
backgroundColor.setFill()
|
||||
rectPath.fill()
|
||||
}
|
||||
|
||||
let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners)
|
||||
context.addPath(path.cgPath)
|
||||
context.clip()
|
||||
base.draw(in: rect)
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a round corner image from on `base` image.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - radius: The round corner radius of creating image.
|
||||
/// - size: The target size of creating image.
|
||||
/// - corners: The target corners which will be applied rounding.
|
||||
/// - backgroundColor: The background color for the output image
|
||||
/// - Returns: An image with round corner of `self`.
|
||||
///
|
||||
/// - Note: This method only works for CG-based image. The current image scale is kept.
|
||||
/// For any non-CG-based image, `base` itself is returned.
|
||||
public func image(
|
||||
withRoundRadius radius: CGFloat,
|
||||
fit size: CGSize,
|
||||
roundingCorners corners: RectCorner = .all,
|
||||
backgroundColor: KFCrossPlatformColor? = nil
|
||||
) -> KFCrossPlatformImage
|
||||
{
|
||||
image(withRadius: .point(radius), fit: size, roundingCorners: corners, backgroundColor: backgroundColor)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> NSBezierPath {
|
||||
let cornerRadius = radius.compute(with: rect.size)
|
||||
let path = NSBezierPath(roundedRect: rect, byRoundingCorners: corners, radius: cornerRadius - offsetBase / 2)
|
||||
path.windingRule = .evenOdd
|
||||
return path
|
||||
}
|
||||
#else
|
||||
func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> UIBezierPath {
|
||||
let cornerRadius = radius.compute(with: rect.size)
|
||||
return UIBezierPath(
|
||||
roundedRect: rect,
|
||||
byRoundingCorners: corners.uiRectCorner,
|
||||
cornerRadii: CGSize(
|
||||
width: cornerRadius - offsetBase / 2,
|
||||
height: cornerRadius - offsetBase / 2
|
||||
)
|
||||
)
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
func resize(to size: CGSize, for contentMode: UIView.ContentMode) -> KFCrossPlatformImage {
|
||||
switch contentMode {
|
||||
case .scaleAspectFit:
|
||||
return resize(to: size, for: .aspectFit)
|
||||
case .scaleAspectFill:
|
||||
return resize(to: size, for: .aspectFill)
|
||||
default:
|
||||
return resize(to: size)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: Resizing
|
||||
/// Resizes `base` image to an image with new size.
|
||||
///
|
||||
/// - Parameter size: The target size in point.
|
||||
/// - Returns: An image with new size.
|
||||
/// - Note: This method only works for CG-based image. The current image scale is kept.
|
||||
/// For any non-CG-based image, `base` itself is returned.
|
||||
public func resize(to size: CGSize) -> KFCrossPlatformImage {
|
||||
guard let _ = cgImage else {
|
||||
assertionFailure("[Kingfisher] Resize only works for CG-based image.")
|
||||
return base
|
||||
}
|
||||
|
||||
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size)
|
||||
return draw(to: size, inverting: false) { _ in
|
||||
#if os(macOS)
|
||||
base.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0)
|
||||
#else
|
||||
base.draw(in: rect)
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Resizes `base` image to an image of new size, respecting the given content mode.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - targetSize: The target size in point.
|
||||
/// - contentMode: Content mode of output image should be.
|
||||
/// - Returns: An image with new size.
|
||||
///
|
||||
/// - Note: This method only works for CG-based image. The current image scale is kept.
|
||||
/// For any non-CG-based image, `base` itself is returned.
|
||||
public func resize(to targetSize: CGSize, for contentMode: ContentMode) -> KFCrossPlatformImage {
|
||||
let newSize = size.kf.resize(to: targetSize, for: contentMode)
|
||||
return resize(to: newSize)
|
||||
}
|
||||
|
||||
// MARK: Cropping
|
||||
/// Crops `base` image to a new size with a given anchor.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - size: The target size.
|
||||
/// - anchor: The anchor point from which the size should be calculated.
|
||||
/// - Returns: An image with new size.
|
||||
///
|
||||
/// - Note: This method only works for CG-based image. The current image scale is kept.
|
||||
/// For any non-CG-based image, `base` itself is returned.
|
||||
public func crop(to size: CGSize, anchorOn anchor: CGPoint) -> KFCrossPlatformImage {
|
||||
guard let cgImage = cgImage else {
|
||||
assertionFailure("[Kingfisher] Crop only works for CG-based image.")
|
||||
return base
|
||||
}
|
||||
|
||||
let rect = self.size.kf.constrainedRect(for: size, anchor: anchor)
|
||||
guard let image = cgImage.cropping(to: rect.scaled(scale)) else {
|
||||
assertionFailure("[Kingfisher] Cropping image failed.")
|
||||
return base
|
||||
}
|
||||
|
||||
return KingfisherWrapper.image(cgImage: image, scale: scale, refImage: base)
|
||||
}
|
||||
|
||||
// MARK: Blur
|
||||
/// Creates an image with blur effect based on `base` image.
|
||||
///
|
||||
/// - Parameter radius: The blur radius should be used when creating blur effect.
|
||||
/// - Returns: An image with blur effect applied.
|
||||
///
|
||||
/// - Note: This method only works for CG-based image. The current image scale is kept.
|
||||
/// For any non-CG-based image, `base` itself is returned.
|
||||
public func blurred(withRadius radius: CGFloat) -> KFCrossPlatformImage {
|
||||
|
||||
guard let cgImage = cgImage else {
|
||||
assertionFailure("[Kingfisher] Blur only works for CG-based image.")
|
||||
return base
|
||||
}
|
||||
|
||||
// http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement
|
||||
// let d = floor(s * 3*sqrt(2*pi)/4 + 0.5)
|
||||
// if d is odd, use three box-blurs of size 'd', centered on the output pixel.
|
||||
let s = max(radius, 2.0)
|
||||
// We will do blur on a resized image (*0.5), so the blur radius could be half as well.
|
||||
|
||||
// Fix the slow compiling time for Swift 3.
|
||||
// See https://github.com/onevcat/Kingfisher/issues/611
|
||||
let pi2 = 2 * CGFloat.pi
|
||||
let sqrtPi2 = sqrt(pi2)
|
||||
var targetRadius = floor(s * 3.0 * sqrtPi2 / 4.0 + 0.5)
|
||||
|
||||
if targetRadius.isEven { targetRadius += 1 }
|
||||
|
||||
// Determine necessary iteration count by blur radius.
|
||||
let iterations: Int
|
||||
if radius < 0.5 {
|
||||
iterations = 1
|
||||
} else if radius < 1.5 {
|
||||
iterations = 2
|
||||
} else {
|
||||
iterations = 3
|
||||
}
|
||||
|
||||
let w = Int(size.width)
|
||||
let h = Int(size.height)
|
||||
|
||||
func createEffectBuffer(_ context: CGContext) -> vImage_Buffer {
|
||||
let data = context.data
|
||||
let width = vImagePixelCount(context.width)
|
||||
let height = vImagePixelCount(context.height)
|
||||
let rowBytes = context.bytesPerRow
|
||||
|
||||
return vImage_Buffer(data: data, height: height, width: width, rowBytes: rowBytes)
|
||||
}
|
||||
GraphicsContext.begin(size: size, scale: scale)
|
||||
guard let context = GraphicsContext.current(size: size, scale: scale, inverting: true, cgImage: cgImage) else {
|
||||
assertionFailure("[Kingfisher] Failed to create CG context for blurring image.")
|
||||
return base
|
||||
}
|
||||
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: w, height: h))
|
||||
GraphicsContext.end()
|
||||
|
||||
var inBuffer = createEffectBuffer(context)
|
||||
|
||||
GraphicsContext.begin(size: size, scale: scale)
|
||||
guard let outContext = GraphicsContext.current(size: size, scale: scale, inverting: true, cgImage: cgImage) else {
|
||||
assertionFailure("[Kingfisher] Failed to create CG context for blurring image.")
|
||||
return base
|
||||
}
|
||||
defer { GraphicsContext.end() }
|
||||
var outBuffer = createEffectBuffer(outContext)
|
||||
|
||||
for _ in 0 ..< iterations {
|
||||
let flag = vImage_Flags(kvImageEdgeExtend)
|
||||
vImageBoxConvolve_ARGB8888(
|
||||
&inBuffer, &outBuffer, nil, 0, 0, UInt32(targetRadius), UInt32(targetRadius), nil, flag)
|
||||
// Next inBuffer should be the outButter of current iteration
|
||||
(inBuffer, outBuffer) = (outBuffer, inBuffer)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
let result = outContext.makeImage().flatMap {
|
||||
fixedForRetinaPixel(cgImage: $0, to: size)
|
||||
}
|
||||
#else
|
||||
let result = outContext.makeImage().flatMap {
|
||||
KFCrossPlatformImage(cgImage: $0, scale: base.scale, orientation: base.imageOrientation)
|
||||
}
|
||||
#endif
|
||||
guard let blurredImage = result else {
|
||||
assertionFailure("[Kingfisher] Can not make an blurred image within this context.")
|
||||
return base
|
||||
}
|
||||
|
||||
return blurredImage
|
||||
}
|
||||
|
||||
public func addingBorder(_ border: Border) -> KFCrossPlatformImage
|
||||
{
|
||||
guard let _ = cgImage else {
|
||||
assertionFailure("[Kingfisher] Blend mode image only works for CG-based image.")
|
||||
return base
|
||||
}
|
||||
|
||||
let rect = CGRect(origin: .zero, size: size)
|
||||
return draw(to: rect.size, inverting: false) { context in
|
||||
|
||||
#if os(macOS)
|
||||
base.draw(in: rect)
|
||||
#else
|
||||
base.draw(in: rect, blendMode: .normal, alpha: 1.0)
|
||||
#endif
|
||||
|
||||
|
||||
let strokeRect = rect.insetBy(dx: border.lineWidth / 2, dy: border.lineWidth / 2)
|
||||
context.setStrokeColor(border.color.cgColor)
|
||||
context.setAlpha(border.color.rgba.a)
|
||||
|
||||
let line = pathForRoundCorner(
|
||||
rect: strokeRect,
|
||||
radius: border.radius,
|
||||
corners: border.roundingCorners,
|
||||
offsetBase: border.lineWidth
|
||||
)
|
||||
line.lineCapStyle = .square
|
||||
line.lineWidth = border.lineWidth
|
||||
line.stroke()
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Overlay
|
||||
/// Creates an image from `base` image with a color overlay layer.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - color: The color should be use to overlay.
|
||||
/// - fraction: Fraction of input color. From 0.0 to 1.0. 0.0 means solid color,
|
||||
/// 1.0 means transparent overlay.
|
||||
/// - Returns: An image with a color overlay applied.
|
||||
///
|
||||
/// - Note: This method only works for CG-based image. The current image scale is kept.
|
||||
/// For any non-CG-based image, `base` itself is returned.
|
||||
public func overlaying(with color: KFCrossPlatformColor, fraction: CGFloat) -> KFCrossPlatformImage {
|
||||
|
||||
guard let _ = cgImage else {
|
||||
assertionFailure("[Kingfisher] Overlaying only works for CG-based image.")
|
||||
return base
|
||||
}
|
||||
|
||||
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
|
||||
return draw(to: rect.size, inverting: false) { context in
|
||||
#if os(macOS)
|
||||
base.draw(in: rect)
|
||||
if fraction > 0 {
|
||||
color.withAlphaComponent(1 - fraction).set()
|
||||
rect.fill(using: .sourceAtop)
|
||||
}
|
||||
#else
|
||||
color.set()
|
||||
UIRectFill(rect)
|
||||
base.draw(in: rect, blendMode: .destinationIn, alpha: 1.0)
|
||||
|
||||
if fraction > 0 {
|
||||
base.draw(in: rect, blendMode: .sourceAtop, alpha: fraction)
|
||||
}
|
||||
#endif
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Tint
|
||||
/// Creates an image from `base` image with a color tint.
|
||||
///
|
||||
/// - Parameter color: The color should be used to tint `base`
|
||||
/// - Returns: An image with a color tint applied.
|
||||
public func tinted(with color: KFCrossPlatformColor) -> KFCrossPlatformImage {
|
||||
#if os(watchOS)
|
||||
return base
|
||||
#else
|
||||
return apply(.tint(color))
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: Color Control
|
||||
|
||||
/// Create an image from `self` with color control.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - brightness: Brightness changing to image.
|
||||
/// - contrast: Contrast changing to image.
|
||||
/// - saturation: Saturation changing to image.
|
||||
/// - inputEV: InputEV changing to image.
|
||||
/// - Returns: An image with color control applied.
|
||||
public func adjusted(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) -> KFCrossPlatformImage {
|
||||
#if os(watchOS)
|
||||
return base
|
||||
#else
|
||||
return apply(.colorControl((brightness, contrast, saturation, inputEV)))
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Return an image with given scale.
|
||||
///
|
||||
/// - Parameter scale: Target scale factor the new image should have.
|
||||
/// - Returns: The image with target scale. If the base image is already in the scale, `base` will be returned.
|
||||
public func scaled(to scale: CGFloat) -> KFCrossPlatformImage {
|
||||
guard scale != self.scale else {
|
||||
return base
|
||||
}
|
||||
guard let cgImage = cgImage else {
|
||||
assertionFailure("[Kingfisher] Scaling only works for CG-based image.")
|
||||
return base
|
||||
}
|
||||
return KingfisherWrapper.image(cgImage: cgImage, scale: scale, refImage: base)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Decoding Image
|
||||
extension KingfisherWrapper where Base: KFCrossPlatformImage {
|
||||
|
||||
/// Returns the decoded image of the `base` image. It will draw the image in a plain context and return the data
|
||||
/// from it. This could improve the drawing performance when an image is just created from data but not yet
|
||||
/// displayed for the first time.
|
||||
///
|
||||
/// - Note: This method only works for CG-based image. The current image scale is kept.
|
||||
/// For any non-CG-based image or animated image, `base` itself is returned.
|
||||
public var decoded: KFCrossPlatformImage { return decoded(scale: scale) }
|
||||
|
||||
/// Returns decoded image of the `base` image at a given scale. It will draw the image in a plain context and
|
||||
/// return the data from it. This could improve the drawing performance when an image is just created from
|
||||
/// data but not yet displayed for the first time.
|
||||
///
|
||||
/// - Parameter scale: The given scale of target image should be.
|
||||
/// - Returns: The decoded image ready to be displayed.
|
||||
///
|
||||
/// - Note: This method only works for CG-based image. The current image scale is kept.
|
||||
/// For any non-CG-based image or animated image, `base` itself is returned.
|
||||
public func decoded(scale: CGFloat) -> KFCrossPlatformImage {
|
||||
// Prevent animated image (GIF) losing it's images
|
||||
#if os(iOS)
|
||||
if frameSource != nil { return base }
|
||||
#else
|
||||
if images != nil { return base }
|
||||
#endif
|
||||
|
||||
guard let imageRef = cgImage else {
|
||||
assertionFailure("[Kingfisher] Decoding only works for CG-based image.")
|
||||
return base
|
||||
}
|
||||
|
||||
let size = CGSize(width: CGFloat(imageRef.width) / scale, height: CGFloat(imageRef.height) / scale)
|
||||
return draw(to: size, inverting: true, scale: scale) { context in
|
||||
context.draw(imageRef, in: CGRect(origin: .zero, size: size))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns decoded image of the `base` image at a given scale. It will draw the image in a plain context and
|
||||
/// return the data from it. This could improve the drawing performance when an image is just created from
|
||||
/// data but not yet displayed for the first time.
|
||||
///
|
||||
/// - Parameter context: The context for drawing.
|
||||
/// - Returns: The decoded image ready to be displayed.
|
||||
///
|
||||
/// - Note: This method only works for CG-based image. The current image scale is kept.
|
||||
/// For any non-CG-based image or animated image, `base` itself is returned.
|
||||
public func decoded(on context: CGContext) -> KFCrossPlatformImage {
|
||||
// Prevent animated image (GIF) losing it's images
|
||||
#if os(iOS)
|
||||
if frameSource != nil { return base }
|
||||
#else
|
||||
if images != nil { return base }
|
||||
#endif
|
||||
|
||||
guard let refImage = cgImage,
|
||||
let decodedRefImage = refImage.decoded(on: context, scale: scale) else
|
||||
{
|
||||
assertionFailure("[Kingfisher] Decoding only works for CG-based image.")
|
||||
return base
|
||||
}
|
||||
return KingfisherWrapper.image(cgImage: decodedRefImage, scale: scale, refImage: base)
|
||||
}
|
||||
}
|
||||
|
||||
extension CGImage {
|
||||
func decoded(on context: CGContext, scale: CGFloat) -> CGImage? {
|
||||
let size = CGSize(width: CGFloat(self.width) / scale, height: CGFloat(self.height) / scale)
|
||||
context.draw(self, in: CGRect(origin: .zero, size: size))
|
||||
guard let decodedImageRef = context.makeImage() else {
|
||||
return nil
|
||||
}
|
||||
return decodedImageRef
|
||||
}
|
||||
}
|
||||
|
||||
extension KingfisherWrapper where Base: KFCrossPlatformImage {
|
||||
func draw(
|
||||
to size: CGSize,
|
||||
inverting: Bool,
|
||||
scale: CGFloat? = nil,
|
||||
refImage: KFCrossPlatformImage? = nil,
|
||||
draw: (CGContext) -> Bool // Whether use the refImage (`true`) or ignore image orientation (`false`)
|
||||
) -> KFCrossPlatformImage
|
||||
{
|
||||
#if os(macOS) || os(watchOS)
|
||||
let targetScale = scale ?? self.scale
|
||||
GraphicsContext.begin(size: size, scale: targetScale)
|
||||
guard let context = GraphicsContext.current(size: size, scale: targetScale, inverting: inverting, cgImage: cgImage) else {
|
||||
assertionFailure("[Kingfisher] Failed to create CG context for blurring image.")
|
||||
return base
|
||||
}
|
||||
defer { GraphicsContext.end() }
|
||||
let useRefImage = draw(context)
|
||||
guard let cgImage = context.makeImage() else {
|
||||
return base
|
||||
}
|
||||
let ref = useRefImage ? (refImage ?? base) : nil
|
||||
return KingfisherWrapper.image(cgImage: cgImage, scale: targetScale, refImage: ref)
|
||||
#else
|
||||
|
||||
let format = UIGraphicsImageRendererFormat.preferred()
|
||||
format.scale = scale ?? self.scale
|
||||
let renderer = UIGraphicsImageRenderer(size: size, format: format)
|
||||
|
||||
var useRefImage: Bool = false
|
||||
let image = renderer.image { rendererContext in
|
||||
|
||||
let context = rendererContext.cgContext
|
||||
if inverting { // If drawing a CGImage, we need to make context flipped.
|
||||
context.scaleBy(x: 1.0, y: -1.0)
|
||||
context.translateBy(x: 0, y: -size.height)
|
||||
}
|
||||
|
||||
useRefImage = draw(context)
|
||||
}
|
||||
if useRefImage {
|
||||
guard let cgImage = image.cgImage else {
|
||||
return base
|
||||
}
|
||||
let ref = refImage ?? base
|
||||
return KingfisherWrapper.image(cgImage: cgImage, scale: format.scale, refImage: ref)
|
||||
} else {
|
||||
return image
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
func fixedForRetinaPixel(cgImage: CGImage, to size: CGSize) -> KFCrossPlatformImage {
|
||||
|
||||
let image = KFCrossPlatformImage(cgImage: cgImage, size: base.size)
|
||||
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size)
|
||||
|
||||
return draw(to: self.size, inverting: false) { context in
|
||||
image.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0)
|
||||
return false
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
130
Pods/Kingfisher/Sources/Image/ImageFormat.swift
generated
Normal file
130
Pods/Kingfisher/Sources/Image/ImageFormat.swift
generated
Normal file
@@ -0,0 +1,130 @@
|
||||
//
|
||||
// ImageFormat.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2018/09/28.
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Represents image format.
|
||||
///
|
||||
/// - unknown: The format cannot be recognized or not supported yet.
|
||||
/// - PNG: PNG image format.
|
||||
/// - JPEG: JPEG image format.
|
||||
/// - GIF: GIF image format.
|
||||
public enum ImageFormat {
|
||||
/// The format cannot be recognized or not supported yet.
|
||||
case unknown
|
||||
/// PNG image format.
|
||||
case PNG
|
||||
/// JPEG image format.
|
||||
case JPEG
|
||||
/// GIF image format.
|
||||
case GIF
|
||||
|
||||
struct HeaderData {
|
||||
static var PNG: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]
|
||||
static var JPEG_SOI: [UInt8] = [0xFF, 0xD8]
|
||||
static var JPEG_IF: [UInt8] = [0xFF]
|
||||
static var GIF: [UInt8] = [0x47, 0x49, 0x46]
|
||||
}
|
||||
|
||||
/// https://en.wikipedia.org/wiki/JPEG
|
||||
public enum JPEGMarker {
|
||||
case SOF0 //baseline
|
||||
case SOF2 //progressive
|
||||
case DHT //Huffman Table
|
||||
case DQT //Quantization Table
|
||||
case DRI //Restart Interval
|
||||
case SOS //Start Of Scan
|
||||
case RSTn(UInt8) //Restart
|
||||
case APPn //Application-specific
|
||||
case COM //Comment
|
||||
case EOI //End Of Image
|
||||
|
||||
var bytes: [UInt8] {
|
||||
switch self {
|
||||
case .SOF0: return [0xFF, 0xC0]
|
||||
case .SOF2: return [0xFF, 0xC2]
|
||||
case .DHT: return [0xFF, 0xC4]
|
||||
case .DQT: return [0xFF, 0xDB]
|
||||
case .DRI: return [0xFF, 0xDD]
|
||||
case .SOS: return [0xFF, 0xDA]
|
||||
case .RSTn(let n): return [0xFF, 0xD0 + n]
|
||||
case .APPn: return [0xFF, 0xE0]
|
||||
case .COM: return [0xFF, 0xFE]
|
||||
case .EOI: return [0xFF, 0xD9]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Data: KingfisherCompatibleValue {}
|
||||
|
||||
// MARK: - Misc Helpers
|
||||
extension KingfisherWrapper where Base == Data {
|
||||
/// Gets the image format corresponding to the data.
|
||||
public var imageFormat: ImageFormat {
|
||||
guard base.count > 8 else { return .unknown }
|
||||
|
||||
var buffer = [UInt8](repeating: 0, count: 8)
|
||||
base.copyBytes(to: &buffer, count: 8)
|
||||
|
||||
if buffer == ImageFormat.HeaderData.PNG {
|
||||
return .PNG
|
||||
|
||||
} else if buffer[0] == ImageFormat.HeaderData.JPEG_SOI[0],
|
||||
buffer[1] == ImageFormat.HeaderData.JPEG_SOI[1],
|
||||
buffer[2] == ImageFormat.HeaderData.JPEG_IF[0]
|
||||
{
|
||||
return .JPEG
|
||||
|
||||
} else if buffer[0] == ImageFormat.HeaderData.GIF[0],
|
||||
buffer[1] == ImageFormat.HeaderData.GIF[1],
|
||||
buffer[2] == ImageFormat.HeaderData.GIF[2]
|
||||
{
|
||||
return .GIF
|
||||
}
|
||||
|
||||
return .unknown
|
||||
}
|
||||
|
||||
public func contains(jpeg marker: ImageFormat.JPEGMarker) -> Bool {
|
||||
guard imageFormat == .JPEG else {
|
||||
return false
|
||||
}
|
||||
|
||||
let bytes = [UInt8](base)
|
||||
let markerBytes = marker.bytes
|
||||
for (index, item) in bytes.enumerated() where bytes.count > index + 1 {
|
||||
guard
|
||||
item == markerBytes.first,
|
||||
bytes[index + 1] == markerBytes[1] else {
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
922
Pods/Kingfisher/Sources/Image/ImageProcessor.swift
generated
Normal file
922
Pods/Kingfisher/Sources/Image/ImageProcessor.swift
generated
Normal file
@@ -0,0 +1,922 @@
|
||||
//
|
||||
// ImageProcessor.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 2016/08/26.
|
||||
//
|
||||
// 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
|
||||
import CoreGraphics
|
||||
|
||||
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
/// Represents an item which could be processed by an `ImageProcessor`.
|
||||
///
|
||||
/// - image: Input image. The processor should provide a way to apply
|
||||
/// processing on this `image` and return the result image.
|
||||
/// - data: Input data. The processor should provide a way to apply
|
||||
/// processing on this `data` and return the result image.
|
||||
public enum ImageProcessItem {
|
||||
|
||||
/// Input image. The processor should provide a way to apply
|
||||
/// processing on this `image` and return the result image.
|
||||
case image(KFCrossPlatformImage)
|
||||
|
||||
/// Input data. The processor should provide a way to apply
|
||||
/// processing on this `data` and return the result image.
|
||||
case data(Data)
|
||||
}
|
||||
|
||||
/// An `ImageProcessor` would be used to convert some downloaded data to an image.
|
||||
public protocol ImageProcessor {
|
||||
/// Identifier of the processor. It will be used to identify the processor when
|
||||
/// caching and retrieving an image. You might want to make sure that processors with
|
||||
/// same properties/functionality have the same identifiers, so correct processed images
|
||||
/// could be retrieved with proper key.
|
||||
///
|
||||
/// - Note: Do not supply an empty string for a customized processor, which is already reserved by
|
||||
/// the `DefaultImageProcessor`. It is recommended to use a reverse domain name notation string of
|
||||
/// your own for the identifier.
|
||||
var identifier: String { get }
|
||||
|
||||
/// Processes the input `ImageProcessItem` with this processor.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - item: Input item which will be processed by `self`.
|
||||
/// - options: The parsed options when processing the item.
|
||||
/// - Returns: The processed image.
|
||||
///
|
||||
/// - Note: The return value should be `nil` if processing failed while converting an input item to image.
|
||||
/// If `nil` received by the processing caller, an error will be reported and the process flow stops.
|
||||
/// If the processing flow is not critical for your flow, then when the input item is already an image
|
||||
/// (`.image` case) and there is any errors in the processing, you could return the input image itself
|
||||
/// to keep the processing pipeline continuing.
|
||||
/// - Note: Most processor only supports CG-based images. watchOS is not supported for processors containing
|
||||
/// a filter, the input image will be returned directly on watchOS.
|
||||
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?
|
||||
}
|
||||
|
||||
extension ImageProcessor {
|
||||
|
||||
/// Appends an `ImageProcessor` to another. The identifier of the new `ImageProcessor`
|
||||
/// will be "\(self.identifier)|>\(another.identifier)".
|
||||
///
|
||||
/// - Parameter another: An `ImageProcessor` you want to append to `self`.
|
||||
/// - Returns: The new `ImageProcessor` will process the image in the order
|
||||
/// of the two processors concatenated.
|
||||
public func append(another: ImageProcessor) -> ImageProcessor {
|
||||
let newIdentifier = identifier.appending("|>\(another.identifier)")
|
||||
return GeneralProcessor(identifier: newIdentifier) {
|
||||
item, options in
|
||||
if let image = self.process(item: item, options: options) {
|
||||
return another.process(item: .image(image), options: options)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ==(left: ImageProcessor, right: ImageProcessor) -> Bool {
|
||||
return left.identifier == right.identifier
|
||||
}
|
||||
|
||||
func !=(left: ImageProcessor, right: ImageProcessor) -> Bool {
|
||||
return !(left == right)
|
||||
}
|
||||
|
||||
typealias ProcessorImp = ((ImageProcessItem, KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?)
|
||||
struct GeneralProcessor: ImageProcessor {
|
||||
let identifier: String
|
||||
let p: ProcessorImp
|
||||
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
return p(item, options)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default processor. It converts the input data to a valid image.
|
||||
/// Images of .PNG, .JPEG and .GIF format are supported.
|
||||
/// If an image item is given as `.image` case, `DefaultImageProcessor` will
|
||||
/// do nothing on it and return the associated image.
|
||||
public struct DefaultImageProcessor: ImageProcessor {
|
||||
|
||||
/// A default `DefaultImageProcessor` could be used across.
|
||||
public static let `default` = DefaultImageProcessor()
|
||||
|
||||
/// Identifier of the processor.
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public let identifier = ""
|
||||
|
||||
/// Creates a `DefaultImageProcessor`. Use `DefaultImageProcessor.default` to get an instance,
|
||||
/// if you do not have a good reason to create your own `DefaultImageProcessor`.
|
||||
public init() {}
|
||||
|
||||
/// Processes the input `ImageProcessItem` with this processor.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - item: Input item which will be processed by `self`.
|
||||
/// - options: Options when processing the item.
|
||||
/// - Returns: The processed image.
|
||||
///
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
switch item {
|
||||
case .image(let image):
|
||||
return image.kf.scaled(to: options.scaleFactor)
|
||||
case .data(let data):
|
||||
return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the rect corner setting when processing a round corner image.
|
||||
public struct RectCorner: OptionSet {
|
||||
|
||||
/// Raw value of the rect corner.
|
||||
public let rawValue: Int
|
||||
|
||||
/// Represents the top left corner.
|
||||
public static let topLeft = RectCorner(rawValue: 1 << 0)
|
||||
|
||||
/// Represents the top right corner.
|
||||
public static let topRight = RectCorner(rawValue: 1 << 1)
|
||||
|
||||
/// Represents the bottom left corner.
|
||||
public static let bottomLeft = RectCorner(rawValue: 1 << 2)
|
||||
|
||||
/// Represents the bottom right corner.
|
||||
public static let bottomRight = RectCorner(rawValue: 1 << 3)
|
||||
|
||||
/// Represents all corners.
|
||||
public static let all: RectCorner = [.topLeft, .topRight, .bottomLeft, .bottomRight]
|
||||
|
||||
/// Creates a `RectCorner` option set with a given value.
|
||||
///
|
||||
/// - Parameter rawValue: The value represents a certain corner option.
|
||||
public init(rawValue: Int) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
var cornerIdentifier: String {
|
||||
if self == .all {
|
||||
return ""
|
||||
}
|
||||
return "_corner(\(rawValue))"
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
/// Processor for adding an blend mode to images. Only CG-based images are supported.
|
||||
public struct BlendImageProcessor: ImageProcessor {
|
||||
|
||||
/// Identifier of the processor.
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public let identifier: String
|
||||
|
||||
/// Blend Mode will be used to blend the input image.
|
||||
public let blendMode: CGBlendMode
|
||||
|
||||
/// Alpha will be used when blend image.
|
||||
public let alpha: CGFloat
|
||||
|
||||
/// Background color of the output image. If `nil`, it will stay transparent.
|
||||
public let backgroundColor: KFCrossPlatformColor?
|
||||
|
||||
/// Creates a `BlendImageProcessor`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - blendMode: Blend Mode will be used to blend the input image.
|
||||
/// - alpha: Alpha will be used when blend image. From 0.0 to 1.0. 1.0 means solid image,
|
||||
/// 0.0 means transparent image (not visible at all). Default is 1.0.
|
||||
/// - backgroundColor: Background color to apply for the output image. Default is `nil`.
|
||||
public init(blendMode: CGBlendMode, alpha: CGFloat = 1.0, backgroundColor: KFCrossPlatformColor? = nil) {
|
||||
self.blendMode = blendMode
|
||||
self.alpha = alpha
|
||||
self.backgroundColor = backgroundColor
|
||||
var identifier = "com.onevcat.Kingfisher.BlendImageProcessor(\(blendMode.rawValue),\(alpha))"
|
||||
if let color = backgroundColor {
|
||||
identifier.append("_\(color.rgbaDescription)")
|
||||
}
|
||||
self.identifier = identifier
|
||||
}
|
||||
|
||||
/// Processes the input `ImageProcessItem` with this processor.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - item: Input item which will be processed by `self`.
|
||||
/// - options: Options when processing the item.
|
||||
/// - Returns: The processed image.
|
||||
///
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
switch item {
|
||||
case .image(let image):
|
||||
return image.kf.scaled(to: options.scaleFactor)
|
||||
.kf.image(withBlendMode: blendMode, alpha: alpha, backgroundColor: backgroundColor)
|
||||
case .data:
|
||||
return (DefaultImageProcessor.default |> self).process(item: item, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
/// Processor for adding an compositing operation to images. Only CG-based images are supported in macOS.
|
||||
public struct CompositingImageProcessor: ImageProcessor {
|
||||
|
||||
/// Identifier of the processor.
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public let identifier: String
|
||||
|
||||
/// Compositing operation will be used to the input image.
|
||||
public let compositingOperation: NSCompositingOperation
|
||||
|
||||
/// Alpha will be used when compositing image.
|
||||
public let alpha: CGFloat
|
||||
|
||||
/// Background color of the output image. If `nil`, it will stay transparent.
|
||||
public let backgroundColor: KFCrossPlatformColor?
|
||||
|
||||
/// Creates a `CompositingImageProcessor`
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - compositingOperation: Compositing operation will be used to the input image.
|
||||
/// - alpha: Alpha will be used when compositing image.
|
||||
/// From 0.0 to 1.0. 1.0 means solid image, 0.0 means transparent image.
|
||||
/// Default is 1.0.
|
||||
/// - backgroundColor: Background color to apply for the output image. Default is `nil`.
|
||||
public init(compositingOperation: NSCompositingOperation,
|
||||
alpha: CGFloat = 1.0,
|
||||
backgroundColor: KFCrossPlatformColor? = nil)
|
||||
{
|
||||
self.compositingOperation = compositingOperation
|
||||
self.alpha = alpha
|
||||
self.backgroundColor = backgroundColor
|
||||
var identifier = "com.onevcat.Kingfisher.CompositingImageProcessor(\(compositingOperation.rawValue),\(alpha))"
|
||||
if let color = backgroundColor {
|
||||
identifier.append("_\(color.rgbaDescription)")
|
||||
}
|
||||
self.identifier = identifier
|
||||
}
|
||||
|
||||
/// Processes the input `ImageProcessItem` with this processor.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - item: Input item which will be processed by `self`.
|
||||
/// - options: Options when processing the item.
|
||||
/// - Returns: The processed image.
|
||||
///
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
switch item {
|
||||
case .image(let image):
|
||||
return image.kf.scaled(to: options.scaleFactor)
|
||||
.kf.image(
|
||||
withCompositingOperation: compositingOperation,
|
||||
alpha: alpha,
|
||||
backgroundColor: backgroundColor)
|
||||
case .data:
|
||||
return (DefaultImageProcessor.default |> self).process(item: item, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Represents a radius specified in a `RoundCornerImageProcessor`.
|
||||
public enum Radius {
|
||||
/// The radius should be calculated as a fraction of the image width. Typically the associated value should be
|
||||
/// between 0 and 0.5, where 0 represents no radius and 0.5 represents using half of the image width.
|
||||
case widthFraction(CGFloat)
|
||||
/// The radius should be calculated as a fraction of the image height. Typically the associated value should be
|
||||
/// between 0 and 0.5, where 0 represents no radius and 0.5 represents using half of the image height.
|
||||
case heightFraction(CGFloat)
|
||||
/// Use a fixed point value as the round corner radius.
|
||||
case point(CGFloat)
|
||||
|
||||
var radiusIdentifier: String {
|
||||
switch self {
|
||||
case .widthFraction(let f):
|
||||
return "w_frac_\(f)"
|
||||
case .heightFraction(let f):
|
||||
return "h_frac_\(f)"
|
||||
case .point(let p):
|
||||
return p.description
|
||||
}
|
||||
}
|
||||
|
||||
public func compute(with size: CGSize) -> CGFloat {
|
||||
let cornerRadius: CGFloat
|
||||
switch self {
|
||||
case .point(let point):
|
||||
cornerRadius = point
|
||||
case .widthFraction(let widthFraction):
|
||||
cornerRadius = size.width * widthFraction
|
||||
case .heightFraction(let heightFraction):
|
||||
cornerRadius = size.height * heightFraction
|
||||
}
|
||||
return cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
/// Processor for making round corner images. Only CG-based images are supported in macOS,
|
||||
/// if a non-CG image passed in, the processor will do nothing.
|
||||
///
|
||||
/// - Note: The input image will be rendered with round corner pixels removed. If the image itself does not contain
|
||||
/// alpha channel (for example, a JPEG image), the processed image will contain an alpha channel in memory in order
|
||||
/// to show correctly. However, when cached to disk, Kingfisher respects the original image format by default. That
|
||||
/// means the alpha channel will be removed for these images. When you load the processed image from cache again, you
|
||||
/// will lose transparent corner.
|
||||
///
|
||||
/// You could use `FormatIndicatedCacheSerializer.png` to force Kingfisher to serialize the image to PNG format in this
|
||||
/// case.
|
||||
///
|
||||
public struct RoundCornerImageProcessor: ImageProcessor {
|
||||
|
||||
/// Identifier of the processor.
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public let identifier: String
|
||||
|
||||
/// The radius will be applied in processing. Specify a certain point value with `.point`, or a fraction of the
|
||||
/// target image with `.widthFraction`. or `.heightFraction`. For example, given a square image with width and
|
||||
/// height equals, `.widthFraction(0.5)` means use half of the length of size and makes the final image a round one.
|
||||
public let radius: Radius
|
||||
|
||||
/// The target corners which will be applied rounding.
|
||||
public let roundingCorners: RectCorner
|
||||
|
||||
/// Target size of output image should be. If `nil`, the image will keep its original size after processing.
|
||||
public let targetSize: CGSize?
|
||||
|
||||
/// Background color of the output image. If `nil`, it will use a transparent background.
|
||||
public let backgroundColor: KFCrossPlatformColor?
|
||||
|
||||
/// Creates a `RoundCornerImageProcessor`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - cornerRadius: Corner radius in point will be applied in processing.
|
||||
/// - targetSize: Target size of output image should be. If `nil`,
|
||||
/// the image will keep its original size after processing.
|
||||
/// Default is `nil`.
|
||||
/// - corners: The target corners which will be applied rounding. Default is `.all`.
|
||||
/// - backgroundColor: Background color to apply for the output image. Default is `nil`.
|
||||
///
|
||||
/// - Note:
|
||||
///
|
||||
/// This initializer accepts a concrete point value for `cornerRadius`. If you do not know the image size, but still
|
||||
/// want to apply a full round-corner (making the final image a round one), or specify the corner radius as a
|
||||
/// fraction of one dimension of the target image, use the `Radius` version instead.
|
||||
///
|
||||
public init(
|
||||
cornerRadius: CGFloat,
|
||||
targetSize: CGSize? = nil,
|
||||
roundingCorners corners: RectCorner = .all,
|
||||
backgroundColor: KFCrossPlatformColor? = nil
|
||||
)
|
||||
{
|
||||
let radius = Radius.point(cornerRadius)
|
||||
self.init(radius: radius, targetSize: targetSize, roundingCorners: corners, backgroundColor: backgroundColor)
|
||||
}
|
||||
|
||||
/// Creates a `RoundCornerImageProcessor`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - radius: The radius will be applied in processing.
|
||||
/// - targetSize: Target size of output image should be. If `nil`,
|
||||
/// the image will keep its original size after processing.
|
||||
/// Default is `nil`.
|
||||
/// - corners: The target corners which will be applied rounding. Default is `.all`.
|
||||
/// - backgroundColor: Background color to apply for the output image. Default is `nil`.
|
||||
public init(
|
||||
radius: Radius,
|
||||
targetSize: CGSize? = nil,
|
||||
roundingCorners corners: RectCorner = .all,
|
||||
backgroundColor: KFCrossPlatformColor? = nil
|
||||
)
|
||||
{
|
||||
self.radius = radius
|
||||
self.targetSize = targetSize
|
||||
self.roundingCorners = corners
|
||||
self.backgroundColor = backgroundColor
|
||||
|
||||
self.identifier = {
|
||||
var identifier = ""
|
||||
|
||||
if let size = targetSize {
|
||||
identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" +
|
||||
"(\(radius.radiusIdentifier)_\(size)\(corners.cornerIdentifier))"
|
||||
} else {
|
||||
identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" +
|
||||
"(\(radius.radiusIdentifier)\(corners.cornerIdentifier))"
|
||||
}
|
||||
if let backgroundColor = backgroundColor {
|
||||
identifier += "_\(backgroundColor)"
|
||||
}
|
||||
|
||||
return identifier
|
||||
}()
|
||||
}
|
||||
|
||||
/// Processes the input `ImageProcessItem` with this processor.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - item: Input item which will be processed by `self`.
|
||||
/// - options: Options when processing the item.
|
||||
/// - Returns: The processed image.
|
||||
///
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
switch item {
|
||||
case .image(let image):
|
||||
let size = targetSize ?? image.kf.size
|
||||
return image.kf.scaled(to: options.scaleFactor)
|
||||
.kf.image(
|
||||
withRadius: radius,
|
||||
fit: size,
|
||||
roundingCorners: roundingCorners,
|
||||
backgroundColor: backgroundColor)
|
||||
case .data:
|
||||
return (DefaultImageProcessor.default |> self).process(item: item, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Border {
|
||||
public var color: KFCrossPlatformColor
|
||||
public var lineWidth: CGFloat
|
||||
|
||||
/// The radius will be applied in processing. Specify a certain point value with `.point`, or a fraction of the
|
||||
/// target image with `.widthFraction`. or `.heightFraction`. For example, given a square image with width and
|
||||
/// height equals, `.widthFraction(0.5)` means use half of the length of size and makes the final image a round one.
|
||||
public var radius: Radius
|
||||
|
||||
/// The target corners which will be applied rounding.
|
||||
public var roundingCorners: RectCorner
|
||||
|
||||
public init(
|
||||
color: KFCrossPlatformColor = .black,
|
||||
lineWidth: CGFloat = 4,
|
||||
radius: Radius = .point(0),
|
||||
roundingCorners: RectCorner = .all
|
||||
) {
|
||||
self.color = color
|
||||
self.lineWidth = lineWidth
|
||||
self.radius = radius
|
||||
self.roundingCorners = roundingCorners
|
||||
}
|
||||
|
||||
var identifier: String {
|
||||
"\(color.rgbaDescription)_\(lineWidth)_\(radius.radiusIdentifier)_\(roundingCorners.cornerIdentifier)"
|
||||
}
|
||||
}
|
||||
|
||||
public struct BorderImageProcessor: ImageProcessor {
|
||||
public var identifier: String { "com.onevcat.Kingfisher.RoundCornerImageProcessor(\(border)" }
|
||||
public let border: Border
|
||||
|
||||
public init(border: Border) {
|
||||
self.border = border
|
||||
}
|
||||
|
||||
public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
switch item {
|
||||
case .image(let image):
|
||||
return image.kf.addingBorder(border)
|
||||
case .data:
|
||||
return (DefaultImageProcessor.default |> self).process(item: item, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents how a size adjusts itself to fit a target size.
|
||||
///
|
||||
/// - none: Not scale the content.
|
||||
/// - aspectFit: Scales the content to fit the size of the view by maintaining the aspect ratio.
|
||||
/// - aspectFill: Scales the content to fill the size of the view.
|
||||
public enum ContentMode {
|
||||
/// Not scale the content.
|
||||
case none
|
||||
/// Scales the content to fit the size of the view by maintaining the aspect ratio.
|
||||
case aspectFit
|
||||
/// Scales the content to fill the size of the view.
|
||||
case aspectFill
|
||||
}
|
||||
|
||||
/// Processor for resizing images.
|
||||
/// If you need to resize a data represented image to a smaller size, use `DownsamplingImageProcessor`
|
||||
/// instead, which is more efficient and uses less memory.
|
||||
public struct ResizingImageProcessor: ImageProcessor {
|
||||
|
||||
/// Identifier of the processor.
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public let identifier: String
|
||||
|
||||
/// The reference size for resizing operation in point.
|
||||
public let referenceSize: CGSize
|
||||
|
||||
/// Target content mode of output image should be.
|
||||
/// Default is `.none`.
|
||||
public let targetContentMode: ContentMode
|
||||
|
||||
/// Creates a `ResizingImageProcessor`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - referenceSize: The reference size for resizing operation in point.
|
||||
/// - mode: Target content mode of output image should be.
|
||||
///
|
||||
/// - Note:
|
||||
/// The instance of `ResizingImageProcessor` will follow its `mode` property
|
||||
/// and try to resizing the input images to fit or fill the `referenceSize`.
|
||||
/// That means if you are using a `mode` besides of `.none`, you may get an
|
||||
/// image with its size not be the same as the `referenceSize`.
|
||||
///
|
||||
/// **Example**: With input image size: {100, 200},
|
||||
/// `referenceSize`: {100, 100}, `mode`: `.aspectFit`,
|
||||
/// you will get an output image with size of {50, 100}, which "fit"s
|
||||
/// the `referenceSize`.
|
||||
///
|
||||
/// If you need an output image exactly to be a specified size, append or use
|
||||
/// a `CroppingImageProcessor`.
|
||||
public init(referenceSize: CGSize, mode: ContentMode = .none) {
|
||||
self.referenceSize = referenceSize
|
||||
self.targetContentMode = mode
|
||||
|
||||
if mode == .none {
|
||||
self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(referenceSize))"
|
||||
} else {
|
||||
self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(referenceSize), \(mode))"
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes the input `ImageProcessItem` with this processor.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - item: Input item which will be processed by `self`.
|
||||
/// - options: Options when processing the item.
|
||||
/// - Returns: The processed image.
|
||||
///
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
switch item {
|
||||
case .image(let image):
|
||||
return image.kf.scaled(to: options.scaleFactor)
|
||||
.kf.resize(to: referenceSize, for: targetContentMode)
|
||||
case .data:
|
||||
return (DefaultImageProcessor.default |> self).process(item: item, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processor for adding blur effect to images. `Accelerate.framework` is used underhood for
|
||||
/// a better performance. A simulated Gaussian blur with specified blur radius will be applied.
|
||||
public struct BlurImageProcessor: ImageProcessor {
|
||||
|
||||
/// Identifier of the processor.
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public let identifier: String
|
||||
|
||||
/// Blur radius for the simulated Gaussian blur.
|
||||
public let blurRadius: CGFloat
|
||||
|
||||
/// Creates a `BlurImageProcessor`
|
||||
///
|
||||
/// - parameter blurRadius: Blur radius for the simulated Gaussian blur.
|
||||
public init(blurRadius: CGFloat) {
|
||||
self.blurRadius = blurRadius
|
||||
self.identifier = "com.onevcat.Kingfisher.BlurImageProcessor(\(blurRadius))"
|
||||
}
|
||||
|
||||
/// Processes the input `ImageProcessItem` with this processor.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - item: Input item which will be processed by `self`.
|
||||
/// - options: Options when processing the item.
|
||||
/// - Returns: The processed image.
|
||||
///
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
switch item {
|
||||
case .image(let image):
|
||||
let radius = blurRadius * options.scaleFactor
|
||||
return image.kf.scaled(to: options.scaleFactor)
|
||||
.kf.blurred(withRadius: radius)
|
||||
case .data:
|
||||
return (DefaultImageProcessor.default |> self).process(item: item, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processor for adding an overlay to images. Only CG-based images are supported in macOS.
|
||||
public struct OverlayImageProcessor: ImageProcessor {
|
||||
|
||||
/// Identifier of the processor.
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public let identifier: String
|
||||
|
||||
/// Overlay color will be used to overlay the input image.
|
||||
public let overlay: KFCrossPlatformColor
|
||||
|
||||
/// Fraction will be used when overlay the color to image.
|
||||
public let fraction: CGFloat
|
||||
|
||||
/// Creates an `OverlayImageProcessor`
|
||||
///
|
||||
/// - parameter overlay: Overlay color will be used to overlay the input image.
|
||||
/// - parameter fraction: Fraction will be used when overlay the color to image.
|
||||
/// From 0.0 to 1.0. 0.0 means solid color, 1.0 means transparent overlay.
|
||||
public init(overlay: KFCrossPlatformColor, fraction: CGFloat = 0.5) {
|
||||
self.overlay = overlay
|
||||
self.fraction = fraction
|
||||
self.identifier = "com.onevcat.Kingfisher.OverlayImageProcessor(\(overlay.rgbaDescription)_\(fraction))"
|
||||
}
|
||||
|
||||
/// Processes the input `ImageProcessItem` with this processor.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - item: Input item which will be processed by `self`.
|
||||
/// - options: Options when processing the item.
|
||||
/// - Returns: The processed image.
|
||||
///
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
switch item {
|
||||
case .image(let image):
|
||||
return image.kf.scaled(to: options.scaleFactor)
|
||||
.kf.overlaying(with: overlay, fraction: fraction)
|
||||
case .data:
|
||||
return (DefaultImageProcessor.default |> self).process(item: item, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processor for tint images with color. Only CG-based images are supported.
|
||||
public struct TintImageProcessor: ImageProcessor {
|
||||
|
||||
/// Identifier of the processor.
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public let identifier: String
|
||||
|
||||
/// Tint color will be used to tint the input image.
|
||||
public let tint: KFCrossPlatformColor
|
||||
|
||||
/// Creates a `TintImageProcessor`
|
||||
///
|
||||
/// - parameter tint: Tint color will be used to tint the input image.
|
||||
public init(tint: KFCrossPlatformColor) {
|
||||
self.tint = tint
|
||||
self.identifier = "com.onevcat.Kingfisher.TintImageProcessor(\(tint.rgbaDescription))"
|
||||
}
|
||||
|
||||
/// Processes the input `ImageProcessItem` with this processor.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - item: Input item which will be processed by `self`.
|
||||
/// - options: Options when processing the item.
|
||||
/// - Returns: The processed image.
|
||||
///
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
switch item {
|
||||
case .image(let image):
|
||||
return image.kf.scaled(to: options.scaleFactor)
|
||||
.kf.tinted(with: tint)
|
||||
case .data:
|
||||
return (DefaultImageProcessor.default |> self).process(item: item, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processor for applying some color control to images. Only CG-based images are supported.
|
||||
/// watchOS is not supported.
|
||||
public struct ColorControlsProcessor: ImageProcessor {
|
||||
|
||||
/// Identifier of the processor.
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public let identifier: String
|
||||
|
||||
/// Brightness changing to image.
|
||||
public let brightness: CGFloat
|
||||
|
||||
/// Contrast changing to image.
|
||||
public let contrast: CGFloat
|
||||
|
||||
/// Saturation changing to image.
|
||||
public let saturation: CGFloat
|
||||
|
||||
/// InputEV changing to image.
|
||||
public let inputEV: CGFloat
|
||||
|
||||
/// Creates a `ColorControlsProcessor`
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - brightness: Brightness changing to image.
|
||||
/// - contrast: Contrast changing to image.
|
||||
/// - saturation: Saturation changing to image.
|
||||
/// - inputEV: InputEV changing to image.
|
||||
public init(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) {
|
||||
self.brightness = brightness
|
||||
self.contrast = contrast
|
||||
self.saturation = saturation
|
||||
self.inputEV = inputEV
|
||||
self.identifier = "com.onevcat.Kingfisher.ColorControlsProcessor(\(brightness)_\(contrast)_\(saturation)_\(inputEV))"
|
||||
}
|
||||
|
||||
/// Processes the input `ImageProcessItem` with this processor.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - item: Input item which will be processed by `self`.
|
||||
/// - options: Options when processing the item.
|
||||
/// - Returns: The processed image.
|
||||
///
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
switch item {
|
||||
case .image(let image):
|
||||
return image.kf.scaled(to: options.scaleFactor)
|
||||
.kf.adjusted(brightness: brightness, contrast: contrast, saturation: saturation, inputEV: inputEV)
|
||||
case .data:
|
||||
return (DefaultImageProcessor.default |> self).process(item: item, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processor for applying black and white effect to images. Only CG-based images are supported.
|
||||
/// watchOS is not supported.
|
||||
public struct BlackWhiteProcessor: ImageProcessor {
|
||||
|
||||
/// Identifier of the processor.
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public let identifier = "com.onevcat.Kingfisher.BlackWhiteProcessor"
|
||||
|
||||
/// Creates a `BlackWhiteProcessor`
|
||||
public init() {}
|
||||
|
||||
/// Processes the input `ImageProcessItem` with this processor.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - item: Input item which will be processed by `self`.
|
||||
/// - options: Options when processing the item.
|
||||
/// - Returns: The processed image.
|
||||
///
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
return ColorControlsProcessor(brightness: 0.0, contrast: 1.0, saturation: 0.0, inputEV: 0.7)
|
||||
.process(item: item, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
/// Processor for cropping an image. Only CG-based images are supported.
|
||||
/// watchOS is not supported.
|
||||
public struct CroppingImageProcessor: ImageProcessor {
|
||||
|
||||
/// Identifier of the processor.
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public let identifier: String
|
||||
|
||||
/// Target size of output image should be.
|
||||
public let size: CGSize
|
||||
|
||||
/// Anchor point from which the output size should be calculate.
|
||||
/// The anchor point is consisted by two values between 0.0 and 1.0.
|
||||
/// It indicates a related point in current image.
|
||||
/// See `CroppingImageProcessor.init(size:anchor:)` for more.
|
||||
public let anchor: CGPoint
|
||||
|
||||
/// Creates a `CroppingImageProcessor`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - size: Target size of output image should be.
|
||||
/// - anchor: The anchor point from which the size should be calculated.
|
||||
/// Default is `CGPoint(x: 0.5, y: 0.5)`, which means the center of input image.
|
||||
/// - Note:
|
||||
/// The anchor point is consisted by two values between 0.0 and 1.0.
|
||||
/// It indicates a related point in current image, eg: (0.0, 0.0) for top-left
|
||||
/// corner, (0.5, 0.5) for center and (1.0, 1.0) for bottom-right corner.
|
||||
/// The `size` property of `CroppingImageProcessor` will be used along with
|
||||
/// `anchor` to calculate a target rectangle in the size of image.
|
||||
///
|
||||
/// The target size will be automatically calculated with a reasonable behavior.
|
||||
/// For example, when you have an image size of `CGSize(width: 100, height: 100)`,
|
||||
/// and a target size of `CGSize(width: 20, height: 20)`:
|
||||
/// - with a (0.0, 0.0) anchor (top-left), the crop rect will be `{0, 0, 20, 20}`;
|
||||
/// - with a (0.5, 0.5) anchor (center), it will be `{40, 40, 20, 20}`
|
||||
/// - while with a (1.0, 1.0) anchor (bottom-right), it will be `{80, 80, 20, 20}`
|
||||
public init(size: CGSize, anchor: CGPoint = CGPoint(x: 0.5, y: 0.5)) {
|
||||
self.size = size
|
||||
self.anchor = anchor
|
||||
self.identifier = "com.onevcat.Kingfisher.CroppingImageProcessor(\(size)_\(anchor))"
|
||||
}
|
||||
|
||||
/// Processes the input `ImageProcessItem` with this processor.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - item: Input item which will be processed by `self`.
|
||||
/// - options: Options when processing the item.
|
||||
/// - Returns: The processed image.
|
||||
///
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
switch item {
|
||||
case .image(let image):
|
||||
return image.kf.scaled(to: options.scaleFactor)
|
||||
.kf.crop(to: size, anchorOn: anchor)
|
||||
case .data: return (DefaultImageProcessor.default |> self).process(item: item, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processor for downsampling an image. Compared to `ResizingImageProcessor`, this processor
|
||||
/// does not render the images to resize. Instead, it downsamples the input data directly to an
|
||||
/// image. It is a more efficient than `ResizingImageProcessor`. Prefer to use `DownsamplingImageProcessor` as possible
|
||||
/// as you can than the `ResizingImageProcessor`.
|
||||
///
|
||||
/// Only CG-based images are supported. Animated images (like GIF) is not supported.
|
||||
public struct DownsamplingImageProcessor: ImageProcessor {
|
||||
|
||||
/// Target size of output image should be. It should be smaller than the size of
|
||||
/// input image. If it is larger, the result image will be the same size of input
|
||||
/// data without downsampling.
|
||||
public let size: CGSize
|
||||
|
||||
/// Identifier of the processor.
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public let identifier: String
|
||||
|
||||
/// Creates a `DownsamplingImageProcessor`.
|
||||
///
|
||||
/// - Parameter size: The target size of the downsample operation.
|
||||
public init(size: CGSize) {
|
||||
self.size = size
|
||||
self.identifier = "com.onevcat.Kingfisher.DownsamplingImageProcessor(\(size))"
|
||||
}
|
||||
|
||||
/// Processes the input `ImageProcessItem` with this processor.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - item: Input item which will be processed by `self`.
|
||||
/// - options: Options when processing the item.
|
||||
/// - Returns: The processed image.
|
||||
///
|
||||
/// - Note: See documentation of `ImageProcessor` protocol for more.
|
||||
public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
|
||||
switch item {
|
||||
case .image(let image):
|
||||
guard let data = image.kf.data(format: .unknown) else {
|
||||
return nil
|
||||
}
|
||||
return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor)
|
||||
case .data(let data):
|
||||
return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
infix operator |>: AdditionPrecedence
|
||||
public func |>(left: ImageProcessor, right: ImageProcessor) -> ImageProcessor {
|
||||
return left.append(another: right)
|
||||
}
|
||||
|
||||
extension KFCrossPlatformColor {
|
||||
|
||||
var rgba: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {
|
||||
var r: CGFloat = 0
|
||||
var g: CGFloat = 0
|
||||
var b: CGFloat = 0
|
||||
var a: CGFloat = 0
|
||||
|
||||
#if os(macOS)
|
||||
(usingColorSpace(.extendedSRGB) ?? self).getRed(&r, green: &g, blue: &b, alpha: &a)
|
||||
#else
|
||||
getRed(&r, green: &g, blue: &b, alpha: &a)
|
||||
#endif
|
||||
|
||||
return (r, g, b, a)
|
||||
}
|
||||
|
||||
var rgbaDescription: String {
|
||||
let components = self.rgba
|
||||
return String(format: "(%.2f,%.2f,%.2f,%.2f)", components.r, components.g, components.b, components.a)
|
||||
}
|
||||
}
|
||||
348
Pods/Kingfisher/Sources/Image/ImageProgressive.swift
generated
Normal file
348
Pods/Kingfisher/Sources/Image/ImageProgressive.swift
generated
Normal file
@@ -0,0 +1,348 @@
|
||||
//
|
||||
// ImageProgressive.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by lixiang on 2019/5/10.
|
||||
//
|
||||
// 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
|
||||
import CoreGraphics
|
||||
|
||||
private let sharedProcessingQueue: CallbackQueue =
|
||||
.dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process"))
|
||||
|
||||
public struct ImageProgressive {
|
||||
|
||||
/// The updating strategy when an intermediate progressive image is generated and about to be set to the hosting view.
|
||||
///
|
||||
/// - default: Use the progressive image as it is. It is the standard behavior when handling the progressive image.
|
||||
/// - keepCurrent: Discard this progressive image and keep the current displayed one.
|
||||
/// - replace: Replace the image to a new one. If the progressive loading is initialized by a view extension in
|
||||
/// Kingfisher, the replacing image will be used to update the view.
|
||||
public enum UpdatingStrategy {
|
||||
case `default`
|
||||
case keepCurrent
|
||||
case replace(KFCrossPlatformImage?)
|
||||
}
|
||||
|
||||
/// A default `ImageProgressive` could be used across. It blurs the progressive loading with the fastest
|
||||
/// scan enabled and scan interval as 0.
|
||||
@available(*, deprecated, message: "Getting a default `ImageProgressive` is deprecated due to its syntax symatic is not clear. Use `ImageProgressive.init` instead.", renamed: "init()")
|
||||
public static let `default` = ImageProgressive(
|
||||
isBlur: true,
|
||||
isFastestScan: true,
|
||||
scanInterval: 0
|
||||
)
|
||||
|
||||
/// Whether to enable blur effect processing
|
||||
let isBlur: Bool
|
||||
/// Whether to enable the fastest scan
|
||||
let isFastestScan: Bool
|
||||
/// Minimum time interval for each scan
|
||||
let scanInterval: TimeInterval
|
||||
|
||||
/// Called when an intermediate image is prepared and about to be set to the image view. The return value of this
|
||||
/// delegate will be used to update the hosting view, if any. Otherwise, if there is no hosting view (a.k.a the
|
||||
/// image retrieving is not happening from a view extension method), the returned `UpdatingStrategy` is ignored.
|
||||
public let onImageUpdated = Delegate<KFCrossPlatformImage, UpdatingStrategy>()
|
||||
|
||||
/// Creates an `ImageProgressive` value with default sets. It blurs the progressive loading with the fastest
|
||||
/// scan enabled and scan interval as 0.
|
||||
public init() {
|
||||
self.init(isBlur: true, isFastestScan: true, scanInterval: 0)
|
||||
}
|
||||
|
||||
/// Creates an `ImageProgressive` value the given values.
|
||||
/// - Parameters:
|
||||
/// - isBlur: Whether to enable blur effect processing.
|
||||
/// - isFastestScan: Whether to enable the fastest scan.
|
||||
/// - scanInterval: Minimum time interval for each scan.
|
||||
public init(isBlur: Bool,
|
||||
isFastestScan: Bool,
|
||||
scanInterval: TimeInterval
|
||||
)
|
||||
{
|
||||
self.isBlur = isBlur
|
||||
self.isFastestScan = isFastestScan
|
||||
self.scanInterval = scanInterval
|
||||
}
|
||||
}
|
||||
|
||||
final class ImageProgressiveProvider: DataReceivingSideEffect {
|
||||
|
||||
var onShouldApply: () -> Bool = { return true }
|
||||
|
||||
func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) {
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard self.onShouldApply() else { return }
|
||||
self.update(data: task.mutableData, with: task.callbacks)
|
||||
}
|
||||
}
|
||||
|
||||
private let option: ImageProgressive
|
||||
private let refresh: (KFCrossPlatformImage) -> Void
|
||||
|
||||
private let decoder: ImageProgressiveDecoder
|
||||
private let queue = ImageProgressiveSerialQueue()
|
||||
|
||||
init?(_ options: KingfisherParsedOptionsInfo,
|
||||
refresh: @escaping (KFCrossPlatformImage) -> Void) {
|
||||
guard let option = options.progressiveJPEG else { return nil }
|
||||
|
||||
self.option = option
|
||||
self.refresh = refresh
|
||||
self.decoder = ImageProgressiveDecoder(
|
||||
option,
|
||||
processingQueue: options.processingQueue ?? sharedProcessingQueue,
|
||||
creatingOptions: options.imageCreatingOptions
|
||||
)
|
||||
}
|
||||
|
||||
func update(data: Data, with callbacks: [SessionDataTask.TaskCallback]) {
|
||||
guard !data.isEmpty else { return }
|
||||
|
||||
queue.add(minimum: option.scanInterval) { completion in
|
||||
|
||||
func decode(_ data: Data) {
|
||||
self.decoder.decode(data, with: callbacks) { image in
|
||||
defer { completion() }
|
||||
guard self.onShouldApply() else { return }
|
||||
guard let image = image else { return }
|
||||
self.refresh(image)
|
||||
}
|
||||
}
|
||||
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var onShouldApply: Bool = false
|
||||
|
||||
CallbackQueue.mainAsync.execute {
|
||||
onShouldApply = self.onShouldApply()
|
||||
semaphore.signal()
|
||||
}
|
||||
semaphore.wait()
|
||||
guard onShouldApply else {
|
||||
self.queue.clean()
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
if self.option.isFastestScan {
|
||||
decode(self.decoder.scanning(data) ?? Data())
|
||||
} else {
|
||||
self.decoder.scanning(data).forEach { decode($0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ImageProgressiveDecoder {
|
||||
|
||||
private let option: ImageProgressive
|
||||
private let processingQueue: CallbackQueue
|
||||
private let creatingOptions: ImageCreatingOptions
|
||||
private(set) var scannedCount = 0
|
||||
private(set) var scannedIndex = -1
|
||||
|
||||
init(_ option: ImageProgressive,
|
||||
processingQueue: CallbackQueue,
|
||||
creatingOptions: ImageCreatingOptions) {
|
||||
self.option = option
|
||||
self.processingQueue = processingQueue
|
||||
self.creatingOptions = creatingOptions
|
||||
}
|
||||
|
||||
func scanning(_ data: Data) -> [Data] {
|
||||
guard data.kf.contains(jpeg: .SOF2) else {
|
||||
return []
|
||||
}
|
||||
guard scannedIndex + 1 < data.count else {
|
||||
return []
|
||||
}
|
||||
|
||||
var datas: [Data] = []
|
||||
var index = scannedIndex + 1
|
||||
var count = scannedCount
|
||||
|
||||
while index < data.count - 1 {
|
||||
scannedIndex = index
|
||||
// 0xFF, 0xDA - Start Of Scan
|
||||
let SOS = ImageFormat.JPEGMarker.SOS.bytes
|
||||
if data[index] == SOS[0], data[index + 1] == SOS[1] {
|
||||
if count > 0 {
|
||||
datas.append(data[0 ..< index])
|
||||
}
|
||||
count += 1
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
|
||||
// Found more scans this the previous time
|
||||
guard count > scannedCount else { return [] }
|
||||
scannedCount = count
|
||||
|
||||
// `> 1` checks that we've received a first scan (SOS) and then received
|
||||
// and also received a second scan (SOS). This way we know that we have
|
||||
// at least one full scan available.
|
||||
guard count > 1 else { return [] }
|
||||
return datas
|
||||
}
|
||||
|
||||
func scanning(_ data: Data) -> Data? {
|
||||
guard data.kf.contains(jpeg: .SOF2) else {
|
||||
return nil
|
||||
}
|
||||
guard scannedIndex + 1 < data.count else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var index = scannedIndex + 1
|
||||
var count = scannedCount
|
||||
var lastSOSIndex = 0
|
||||
|
||||
while index < data.count - 1 {
|
||||
scannedIndex = index
|
||||
// 0xFF, 0xDA - Start Of Scan
|
||||
let SOS = ImageFormat.JPEGMarker.SOS.bytes
|
||||
if data[index] == SOS[0], data[index + 1] == SOS[1] {
|
||||
lastSOSIndex = index
|
||||
count += 1
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
|
||||
// Found more scans this the previous time
|
||||
guard count > scannedCount else { return nil }
|
||||
scannedCount = count
|
||||
|
||||
// `> 1` checks that we've received a first scan (SOS) and then received
|
||||
// and also received a second scan (SOS). This way we know that we have
|
||||
// at least one full scan available.
|
||||
guard count > 1 && lastSOSIndex > 0 else { return nil }
|
||||
return data[0 ..< lastSOSIndex]
|
||||
}
|
||||
|
||||
func decode(_ data: Data,
|
||||
with callbacks: [SessionDataTask.TaskCallback],
|
||||
completion: @escaping (KFCrossPlatformImage?) -> Void) {
|
||||
guard data.kf.contains(jpeg: .SOF2) else {
|
||||
CallbackQueue.mainCurrentOrAsync.execute { completion(nil) }
|
||||
return
|
||||
}
|
||||
|
||||
func processing(_ data: Data) {
|
||||
let processor = ImageDataProcessor(
|
||||
data: data,
|
||||
callbacks: callbacks,
|
||||
processingQueue: processingQueue
|
||||
)
|
||||
processor.onImageProcessed.delegate(on: self) { (self, result) in
|
||||
guard let image = try? result.0.get() else {
|
||||
CallbackQueue.mainCurrentOrAsync.execute { completion(nil) }
|
||||
return
|
||||
}
|
||||
|
||||
CallbackQueue.mainCurrentOrAsync.execute { completion(image) }
|
||||
}
|
||||
processor.process()
|
||||
}
|
||||
|
||||
// Blur partial images.
|
||||
let count = scannedCount
|
||||
|
||||
if option.isBlur, count < 6 {
|
||||
processingQueue.execute {
|
||||
// Progressively reduce blur as we load more scans.
|
||||
let image = KingfisherWrapper<KFCrossPlatformImage>.image(
|
||||
data: data,
|
||||
options: self.creatingOptions
|
||||
)
|
||||
let radius = max(2, 14 - count * 4)
|
||||
let temp = image?.kf.blurred(withRadius: CGFloat(radius))
|
||||
processing(temp?.kf.data(format: .JPEG) ?? data)
|
||||
}
|
||||
|
||||
} else {
|
||||
processing(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ImageProgressiveSerialQueue {
|
||||
typealias ClosureCallback = ((@escaping () -> Void)) -> Void
|
||||
|
||||
private let queue: DispatchQueue
|
||||
private var items: [DispatchWorkItem] = []
|
||||
private var notify: (() -> Void)?
|
||||
private var lastTime: TimeInterval?
|
||||
|
||||
init() {
|
||||
self.queue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageProgressive.SerialQueue")
|
||||
}
|
||||
|
||||
func add(minimum interval: TimeInterval, closure: @escaping ClosureCallback) {
|
||||
let completion = { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.queue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard !self.items.isEmpty else { return }
|
||||
|
||||
self.items.removeFirst()
|
||||
|
||||
if let next = self.items.first {
|
||||
self.queue.asyncAfter(
|
||||
deadline: .now() + interval,
|
||||
execute: next
|
||||
)
|
||||
|
||||
} else {
|
||||
self.lastTime = Date().timeIntervalSince1970
|
||||
self.notify?()
|
||||
self.notify = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let item = DispatchWorkItem {
|
||||
closure(completion)
|
||||
}
|
||||
if self.items.isEmpty {
|
||||
let difference = Date().timeIntervalSince1970 - (self.lastTime ?? 0)
|
||||
let delay = difference < interval ? interval - difference : 0
|
||||
self.queue.asyncAfter(deadline: .now() + delay, execute: item)
|
||||
}
|
||||
self.items.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
func clean() {
|
||||
queue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.items.forEach { $0.cancel() }
|
||||
self.items.removeAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
118
Pods/Kingfisher/Sources/Image/ImageTransition.swift
generated
Normal file
118
Pods/Kingfisher/Sources/Image/ImageTransition.swift
generated
Normal file
@@ -0,0 +1,118 @@
|
||||
//
|
||||
// ImageTransition.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 15/9/18.
|
||||
//
|
||||
// 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(iOS) || os(tvOS)
|
||||
import UIKit
|
||||
|
||||
/// Transition effect which will be used when an image downloaded and set by `UIImageView`
|
||||
/// extension API in Kingfisher. You can assign an enum value with transition duration as
|
||||
/// an item in `KingfisherOptionsInfo` to enable the animation transition.
|
||||
///
|
||||
/// Apple's UIViewAnimationOptions is used under the hood.
|
||||
/// For custom transition, you should specified your own transition options, animations and
|
||||
/// completion handler as well.
|
||||
///
|
||||
/// - none: No animation transition.
|
||||
/// - fade: Fade in the loaded image in a given duration.
|
||||
/// - flipFromLeft: Flip from left transition.
|
||||
/// - flipFromRight: Flip from right transition.
|
||||
/// - flipFromTop: Flip from top transition.
|
||||
/// - flipFromBottom: Flip from bottom transition.
|
||||
/// - custom: Custom transition.
|
||||
public enum ImageTransition {
|
||||
/// No animation transition.
|
||||
case none
|
||||
/// Fade in the loaded image in a given duration.
|
||||
case fade(TimeInterval)
|
||||
/// Flip from left transition.
|
||||
case flipFromLeft(TimeInterval)
|
||||
/// Flip from right transition.
|
||||
case flipFromRight(TimeInterval)
|
||||
/// Flip from top transition.
|
||||
case flipFromTop(TimeInterval)
|
||||
/// Flip from bottom transition.
|
||||
case flipFromBottom(TimeInterval)
|
||||
/// Custom transition defined by a general animation block.
|
||||
/// - duration: The time duration of this custom transition.
|
||||
/// - options: `UIView.AnimationOptions` should be used in the transition.
|
||||
/// - animations: The animation block will be applied when setting image.
|
||||
/// - completion: A block called when the transition animation finishes.
|
||||
case custom(duration: TimeInterval,
|
||||
options: UIView.AnimationOptions,
|
||||
animations: ((UIImageView, UIImage) -> Void)?,
|
||||
completion: ((Bool) -> Void)?)
|
||||
|
||||
var duration: TimeInterval {
|
||||
switch self {
|
||||
case .none: return 0
|
||||
case .fade(let duration): return duration
|
||||
|
||||
case .flipFromLeft(let duration): return duration
|
||||
case .flipFromRight(let duration): return duration
|
||||
case .flipFromTop(let duration): return duration
|
||||
case .flipFromBottom(let duration): return duration
|
||||
|
||||
case .custom(let duration, _, _, _): return duration
|
||||
}
|
||||
}
|
||||
|
||||
var animationOptions: UIView.AnimationOptions {
|
||||
switch self {
|
||||
case .none: return []
|
||||
case .fade: return .transitionCrossDissolve
|
||||
|
||||
case .flipFromLeft: return .transitionFlipFromLeft
|
||||
case .flipFromRight: return .transitionFlipFromRight
|
||||
case .flipFromTop: return .transitionFlipFromTop
|
||||
case .flipFromBottom: return .transitionFlipFromBottom
|
||||
|
||||
case .custom(_, let options, _, _): return options
|
||||
}
|
||||
}
|
||||
|
||||
var animations: ((UIImageView, UIImage) -> Void)? {
|
||||
switch self {
|
||||
case .custom(_, _, let animations, _): return animations
|
||||
default: return { $0.image = $1 }
|
||||
}
|
||||
}
|
||||
|
||||
var completion: ((Bool) -> Void)? {
|
||||
switch self {
|
||||
case .custom(_, _, _, let completion): return completion
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
// Just a placeholder for compiling on macOS.
|
||||
public enum ImageTransition {
|
||||
case none
|
||||
/// This is a placeholder on macOS now. It is for SwiftUI (KFImage) to identify the fade option only.
|
||||
case fade(TimeInterval)
|
||||
}
|
||||
#endif
|
||||
82
Pods/Kingfisher/Sources/Image/Placeholder.swift
generated
Normal file
82
Pods/Kingfisher/Sources/Image/Placeholder.swift
generated
Normal file
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// Placeholder.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Tieme van Veen on 28/08/2017.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
/// Represents a placeholder type which could be set while loading as well as
|
||||
/// loading finished without getting an image.
|
||||
public protocol Placeholder {
|
||||
|
||||
/// How the placeholder should be added to a given image view.
|
||||
func add(to imageView: KFCrossPlatformImageView)
|
||||
|
||||
/// How the placeholder should be removed from a given image view.
|
||||
func remove(from imageView: KFCrossPlatformImageView)
|
||||
}
|
||||
|
||||
/// Default implementation of an image placeholder. The image will be set or
|
||||
/// reset directly for `image` property of the image view.
|
||||
extension KFCrossPlatformImage: Placeholder {
|
||||
/// How the placeholder should be added to a given image view.
|
||||
public func add(to imageView: KFCrossPlatformImageView) { imageView.image = self }
|
||||
|
||||
/// How the placeholder should be removed from a given image view.
|
||||
public func remove(from imageView: KFCrossPlatformImageView) { imageView.image = nil }
|
||||
}
|
||||
|
||||
/// Default implementation of an arbitrary view as placeholder. The view will be
|
||||
/// added as a subview when adding and be removed from its super view when removing.
|
||||
///
|
||||
/// To use your customize View type as placeholder, simply let it conforming to
|
||||
/// `Placeholder` by `extension MyView: Placeholder {}`.
|
||||
extension Placeholder where Self: KFCrossPlatformView {
|
||||
|
||||
/// How the placeholder should be added to a given image view.
|
||||
public func add(to imageView: KFCrossPlatformImageView) {
|
||||
imageView.addSubview(self)
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
centerXAnchor.constraint(equalTo: imageView.centerXAnchor).isActive = true
|
||||
centerYAnchor.constraint(equalTo: imageView.centerYAnchor).isActive = true
|
||||
heightAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
|
||||
widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true
|
||||
}
|
||||
|
||||
/// How the placeholder should be removed from a given image view.
|
||||
public func remove(from imageView: KFCrossPlatformImageView) {
|
||||
removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
94
Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift
generated
Normal file
94
Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift
generated
Normal file
@@ -0,0 +1,94 @@
|
||||
//
|
||||
// AuthenticationChallengeResponsable.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 2018/10/11.
|
||||
//
|
||||
// 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
|
||||
|
||||
@available(*, deprecated, message: "Typo. Use `AuthenticationChallengeResponsible` instead", renamed: "AuthenticationChallengeResponsible")
|
||||
public typealias AuthenticationChallengeResponsable = AuthenticationChallengeResponsible
|
||||
|
||||
/// Protocol indicates that an authentication challenge could be handled.
|
||||
public protocol AuthenticationChallengeResponsible: AnyObject {
|
||||
|
||||
/// Called when a session level authentication challenge is received.
|
||||
/// This method provide a chance to handle and response to the authentication
|
||||
/// challenge before downloading could start.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - downloader: The downloader which receives this challenge.
|
||||
/// - challenge: An object that contains the request for authentication.
|
||||
/// - completionHandler: A handler that your delegate method must call.
|
||||
///
|
||||
/// - Note: This method is a forward from `URLSessionDelegate.urlSession(:didReceiveChallenge:completionHandler:)`.
|
||||
/// Please refer to the document of it in `URLSessionDelegate`.
|
||||
func downloader(
|
||||
_ downloader: ImageDownloader,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
|
||||
|
||||
/// Called when a task level authentication challenge is received.
|
||||
/// This method provide a chance to handle and response to the authentication
|
||||
/// challenge before downloading could start.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - downloader: The downloader which receives this challenge.
|
||||
/// - task: The task whose request requires authentication.
|
||||
/// - challenge: An object that contains the request for authentication.
|
||||
/// - completionHandler: A handler that your delegate method must call.
|
||||
func downloader(
|
||||
_ downloader: ImageDownloader,
|
||||
task: URLSessionTask,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
|
||||
}
|
||||
|
||||
extension AuthenticationChallengeResponsible {
|
||||
|
||||
public func downloader(
|
||||
_ downloader: ImageDownloader,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
|
||||
{
|
||||
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
|
||||
if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) {
|
||||
let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
|
||||
completionHandler(.useCredential, credential)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(.performDefaultHandling, nil)
|
||||
}
|
||||
|
||||
public func downloader(
|
||||
_ downloader: ImageDownloader,
|
||||
task: URLSessionTask,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
|
||||
{
|
||||
completionHandler(.performDefaultHandling, nil)
|
||||
}
|
||||
|
||||
}
|
||||
74
Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift
generated
Normal file
74
Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift
generated
Normal file
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// ImageDataProcessor.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 2018/10/11.
|
||||
//
|
||||
// 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
|
||||
|
||||
private let sharedProcessingQueue: CallbackQueue =
|
||||
.dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process"))
|
||||
|
||||
// Handles image processing work on an own process queue.
|
||||
class ImageDataProcessor {
|
||||
let data: Data
|
||||
let callbacks: [SessionDataTask.TaskCallback]
|
||||
let queue: CallbackQueue
|
||||
|
||||
// Note: We have an optimization choice there, to reduce queue dispatch by checking callback
|
||||
// queue settings in each option...
|
||||
let onImageProcessed = Delegate<(Result<KFCrossPlatformImage, KingfisherError>, SessionDataTask.TaskCallback), Void>()
|
||||
|
||||
init(data: Data, callbacks: [SessionDataTask.TaskCallback], processingQueue: CallbackQueue?) {
|
||||
self.data = data
|
||||
self.callbacks = callbacks
|
||||
self.queue = processingQueue ?? sharedProcessingQueue
|
||||
}
|
||||
|
||||
func process() {
|
||||
queue.execute(doProcess)
|
||||
}
|
||||
|
||||
private func doProcess() {
|
||||
var processedImages = [String: KFCrossPlatformImage]()
|
||||
for callback in callbacks {
|
||||
let processor = callback.options.processor
|
||||
var image = processedImages[processor.identifier]
|
||||
if image == nil {
|
||||
image = processor.process(item: .data(data), options: callback.options)
|
||||
processedImages[processor.identifier] = image
|
||||
}
|
||||
|
||||
let result: Result<KFCrossPlatformImage, KingfisherError>
|
||||
if let image = image {
|
||||
let finalImage = callback.options.backgroundDecode ? image.kf.decoded : image
|
||||
result = .success(finalImage)
|
||||
} else {
|
||||
let error = KingfisherError.processorError(
|
||||
reason: .processingFailed(processor: processor, item: .data(data)))
|
||||
result = .failure(error)
|
||||
}
|
||||
onImageProcessed.call((result, callback))
|
||||
}
|
||||
}
|
||||
}
|
||||
502
Pods/Kingfisher/Sources/Networking/ImageDownloader.swift
generated
Normal file
502
Pods/Kingfisher/Sources/Networking/ImageDownloader.swift
generated
Normal file
@@ -0,0 +1,502 @@
|
||||
//
|
||||
// ImageDownloader.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.
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
typealias DownloadResult = Result<ImageLoadingResult, KingfisherError>
|
||||
|
||||
/// Represents a success result of an image downloading progress.
|
||||
public struct ImageLoadingResult {
|
||||
|
||||
/// The downloaded image.
|
||||
public let image: KFCrossPlatformImage
|
||||
|
||||
/// Original URL of the image request.
|
||||
public let url: URL?
|
||||
|
||||
/// The raw data received from downloader.
|
||||
public let originalData: Data
|
||||
|
||||
/// Creates an `ImageDownloadResult`
|
||||
///
|
||||
/// - parameter image: Image of the download result
|
||||
/// - parameter url: URL from where the image was downloaded from
|
||||
/// - parameter originalData: The image's binary data
|
||||
public init(image: KFCrossPlatformImage, url: URL? = nil, originalData: Data) {
|
||||
self.image = image
|
||||
self.url = url
|
||||
self.originalData = originalData
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a task of an image downloading process.
|
||||
public struct DownloadTask {
|
||||
|
||||
/// The `SessionDataTask` object bounded to this download task. Multiple `DownloadTask`s could refer
|
||||
/// to a same `sessionTask`. This is an optimization in Kingfisher to prevent multiple downloading task
|
||||
/// for the same URL resource at the same time.
|
||||
///
|
||||
/// When you `cancel` a `DownloadTask`, this `SessionDataTask` and its cancel token will be pass through.
|
||||
/// You can use them to identify the cancelled task.
|
||||
public let sessionTask: SessionDataTask
|
||||
|
||||
/// The cancel token which is used to cancel the task. This is only for identify the task when it is cancelled.
|
||||
/// To cancel a `DownloadTask`, use `cancel` instead.
|
||||
public let cancelToken: SessionDataTask.CancelToken
|
||||
|
||||
/// Cancel this task if it is running. It will do nothing if this task is not running.
|
||||
///
|
||||
/// - Note:
|
||||
/// In Kingfisher, there is an optimization to prevent starting another download task if the target URL is being
|
||||
/// downloading. However, even when internally no new session task created, a `DownloadTask` will be still created
|
||||
/// and returned when you call related methods, but it will share the session downloading task with a previous task.
|
||||
/// In this case, if multiple `DownloadTask`s share a single session download task, cancelling a `DownloadTask`
|
||||
/// does not affect other `DownloadTask`s.
|
||||
///
|
||||
/// If you need to cancel all `DownloadTask`s of a url, use `ImageDownloader.cancel(url:)`. If you need to cancel
|
||||
/// all downloading tasks of an `ImageDownloader`, use `ImageDownloader.cancelAll()`.
|
||||
public func cancel() {
|
||||
sessionTask.cancel(token: cancelToken)
|
||||
}
|
||||
}
|
||||
|
||||
extension DownloadTask {
|
||||
enum WrappedTask {
|
||||
case download(DownloadTask)
|
||||
case dataProviding
|
||||
|
||||
func cancel() {
|
||||
switch self {
|
||||
case .download(let task): task.cancel()
|
||||
case .dataProviding: break
|
||||
}
|
||||
}
|
||||
|
||||
var value: DownloadTask? {
|
||||
switch self {
|
||||
case .download(let task): return task
|
||||
case .dataProviding: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a downloading manager for requesting the image with a URL from server.
|
||||
open class ImageDownloader {
|
||||
|
||||
// MARK: Singleton
|
||||
/// The default downloader.
|
||||
public static let `default` = ImageDownloader(name: "default")
|
||||
|
||||
// MARK: Public Properties
|
||||
/// The duration before the downloading is timeout. Default is 15 seconds.
|
||||
open var downloadTimeout: TimeInterval = 15.0
|
||||
|
||||
/// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this
|
||||
/// set will be ignored. You can use this set to specify the self-signed site. It only will be used if you don't
|
||||
/// specify the `authenticationChallengeResponder`.
|
||||
///
|
||||
/// If `authenticationChallengeResponder` is set, this property will be ignored and the implementation of
|
||||
/// `authenticationChallengeResponder` will be used instead.
|
||||
open var trustedHosts: Set<String>?
|
||||
|
||||
/// Use this to set supply a configuration for the downloader. By default,
|
||||
/// NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used.
|
||||
///
|
||||
/// You could change the configuration before a downloading task starts.
|
||||
/// A configuration without persistent storage for caches is requested for downloader working correctly.
|
||||
open var sessionConfiguration = URLSessionConfiguration.ephemeral {
|
||||
didSet {
|
||||
session.invalidateAndCancel()
|
||||
session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
|
||||
}
|
||||
}
|
||||
open var sessionDelegate: SessionDelegate {
|
||||
didSet {
|
||||
session.invalidateAndCancel()
|
||||
session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
|
||||
setupSessionHandler()
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the download requests should use pipeline or not. Default is false.
|
||||
open var requestsUsePipelining = false
|
||||
|
||||
/// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
|
||||
open weak var delegate: ImageDownloaderDelegate?
|
||||
|
||||
/// A responder for authentication challenge.
|
||||
/// Downloader will forward the received authentication challenge for the downloading session to this responder.
|
||||
open weak var authenticationChallengeResponder: AuthenticationChallengeResponsible?
|
||||
|
||||
private let name: String
|
||||
private var session: URLSession
|
||||
|
||||
// MARK: Initializers
|
||||
|
||||
/// Creates a downloader with name.
|
||||
///
|
||||
/// - Parameter name: The name for the downloader. It should not be empty.
|
||||
public init(name: String) {
|
||||
if name.isEmpty {
|
||||
fatalError("[Kingfisher] You should specify a name for the downloader. "
|
||||
+ "A downloader with empty name is not permitted.")
|
||||
}
|
||||
|
||||
self.name = name
|
||||
|
||||
sessionDelegate = SessionDelegate()
|
||||
session = URLSession(
|
||||
configuration: sessionConfiguration,
|
||||
delegate: sessionDelegate,
|
||||
delegateQueue: nil)
|
||||
|
||||
authenticationChallengeResponder = self
|
||||
setupSessionHandler()
|
||||
}
|
||||
|
||||
deinit { session.invalidateAndCancel() }
|
||||
|
||||
private func setupSessionHandler() {
|
||||
sessionDelegate.onReceiveSessionChallenge.delegate(on: self) { (self, invoke) in
|
||||
self.authenticationChallengeResponder?.downloader(self, didReceive: invoke.1, completionHandler: invoke.2)
|
||||
}
|
||||
sessionDelegate.onReceiveSessionTaskChallenge.delegate(on: self) { (self, invoke) in
|
||||
self.authenticationChallengeResponder?.downloader(
|
||||
self, task: invoke.1, didReceive: invoke.2, completionHandler: invoke.3)
|
||||
}
|
||||
sessionDelegate.onValidStatusCode.delegate(on: self) { (self, code) in
|
||||
return (self.delegate ?? self).isValidStatusCode(code, for: self)
|
||||
}
|
||||
sessionDelegate.onResponseReceived.delegate(on: self) { (self, invoke) in
|
||||
(self.delegate ?? self).imageDownloader(self, didReceive: invoke.0, completionHandler: invoke.1)
|
||||
}
|
||||
sessionDelegate.onDownloadingFinished.delegate(on: self) { (self, value) in
|
||||
let (url, result) = value
|
||||
do {
|
||||
let value = try result.get()
|
||||
self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: value, error: nil)
|
||||
} catch {
|
||||
self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: nil, error: error)
|
||||
}
|
||||
}
|
||||
sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in
|
||||
return (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, with: task)
|
||||
}
|
||||
}
|
||||
|
||||
// Wraps `completionHandler` to `onCompleted` respectively.
|
||||
private func createCompletionCallBack(_ completionHandler: ((DownloadResult) -> Void)?) -> Delegate<DownloadResult, Void>? {
|
||||
return completionHandler.map { block -> Delegate<DownloadResult, Void> in
|
||||
|
||||
let delegate = Delegate<Result<ImageLoadingResult, KingfisherError>, Void>()
|
||||
delegate.delegate(on: self) { (self, callback) in
|
||||
block(callback)
|
||||
}
|
||||
return delegate
|
||||
}
|
||||
}
|
||||
|
||||
private func createTaskCallback(
|
||||
_ completionHandler: ((DownloadResult) -> Void)?,
|
||||
options: KingfisherParsedOptionsInfo
|
||||
) -> SessionDataTask.TaskCallback
|
||||
{
|
||||
return SessionDataTask.TaskCallback(
|
||||
onCompleted: createCompletionCallBack(completionHandler),
|
||||
options: options
|
||||
)
|
||||
}
|
||||
|
||||
private func createDownloadContext(
|
||||
with url: URL,
|
||||
options: KingfisherParsedOptionsInfo,
|
||||
done: @escaping ((Result<DownloadingContext, KingfisherError>) -> Void)
|
||||
)
|
||||
{
|
||||
func checkRequestAndDone(r: URLRequest) {
|
||||
|
||||
// There is a possibility that request modifier changed the url to `nil` or empty.
|
||||
// In this case, throw an error.
|
||||
guard let url = r.url, !url.absoluteString.isEmpty else {
|
||||
done(.failure(KingfisherError.requestError(reason: .invalidURL(request: r))))
|
||||
return
|
||||
}
|
||||
|
||||
done(.success(DownloadingContext(url: url, request: r, options: options)))
|
||||
}
|
||||
|
||||
// Creates default request.
|
||||
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout)
|
||||
request.httpShouldUsePipelining = requestsUsePipelining
|
||||
if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) , options.lowDataModeSource != nil {
|
||||
request.allowsConstrainedNetworkAccess = false
|
||||
}
|
||||
|
||||
if let requestModifier = options.requestModifier {
|
||||
// Modifies request before sending.
|
||||
requestModifier.modified(for: request) { result in
|
||||
guard let finalRequest = result else {
|
||||
done(.failure(KingfisherError.requestError(reason: .emptyRequest)))
|
||||
return
|
||||
}
|
||||
checkRequestAndDone(r: finalRequest)
|
||||
}
|
||||
} else {
|
||||
checkRequestAndDone(r: request)
|
||||
}
|
||||
}
|
||||
|
||||
private func addDownloadTask(
|
||||
context: DownloadingContext,
|
||||
callback: SessionDataTask.TaskCallback
|
||||
) -> DownloadTask
|
||||
{
|
||||
// Ready to start download. Add it to session task manager (`sessionHandler`)
|
||||
let downloadTask: DownloadTask
|
||||
if let existingTask = sessionDelegate.task(for: context.url) {
|
||||
downloadTask = sessionDelegate.append(existingTask, callback: callback)
|
||||
} else {
|
||||
let sessionDataTask = session.dataTask(with: context.request)
|
||||
sessionDataTask.priority = context.options.downloadPriority
|
||||
downloadTask = sessionDelegate.add(sessionDataTask, url: context.url, callback: callback)
|
||||
}
|
||||
return downloadTask
|
||||
}
|
||||
|
||||
|
||||
private func reportWillDownloadImage(url: URL, request: URLRequest) {
|
||||
delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
|
||||
}
|
||||
|
||||
private func reportDidDownloadImageData(result: Result<(Data, URLResponse?), KingfisherError>, url: URL) {
|
||||
var response: URLResponse?
|
||||
var err: Error?
|
||||
do {
|
||||
response = try result.get().1
|
||||
} catch {
|
||||
err = error
|
||||
}
|
||||
self.delegate?.imageDownloader(
|
||||
self,
|
||||
didFinishDownloadingImageForURL: url,
|
||||
with: response,
|
||||
error: err
|
||||
)
|
||||
}
|
||||
|
||||
private func reportDidProcessImage(
|
||||
result: Result<KFCrossPlatformImage, KingfisherError>, url: URL, response: URLResponse?
|
||||
)
|
||||
{
|
||||
if let image = try? result.get() {
|
||||
self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func startDownloadTask(
|
||||
context: DownloadingContext,
|
||||
callback: SessionDataTask.TaskCallback
|
||||
) -> DownloadTask
|
||||
{
|
||||
|
||||
let downloadTask = addDownloadTask(context: context, callback: callback)
|
||||
|
||||
let sessionTask = downloadTask.sessionTask
|
||||
guard !sessionTask.started else {
|
||||
return downloadTask
|
||||
}
|
||||
|
||||
sessionTask.onTaskDone.delegate(on: self) { (self, done) in
|
||||
// Underlying downloading finishes.
|
||||
// result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback]
|
||||
let (result, callbacks) = done
|
||||
|
||||
// Before processing the downloaded data.
|
||||
self.reportDidDownloadImageData(result: result, url: context.url)
|
||||
|
||||
switch result {
|
||||
// Download finished. Now process the data to an image.
|
||||
case .success(let (data, response)):
|
||||
let processor = ImageDataProcessor(
|
||||
data: data, callbacks: callbacks, processingQueue: context.options.processingQueue
|
||||
)
|
||||
processor.onImageProcessed.delegate(on: self) { (self, done) in
|
||||
// `onImageProcessed` will be called for `callbacks.count` times, with each
|
||||
// `SessionDataTask.TaskCallback` as the input parameter.
|
||||
// result: Result<Image>, callback: SessionDataTask.TaskCallback
|
||||
let (result, callback) = done
|
||||
|
||||
self.reportDidProcessImage(result: result, url: context.url, response: response)
|
||||
|
||||
let imageResult = result.map { ImageLoadingResult(image: $0, url: context.url, originalData: data) }
|
||||
let queue = callback.options.callbackQueue
|
||||
queue.execute { callback.onCompleted?.call(imageResult) }
|
||||
}
|
||||
processor.process()
|
||||
|
||||
case .failure(let error):
|
||||
callbacks.forEach { callback in
|
||||
let queue = callback.options.callbackQueue
|
||||
queue.execute { callback.onCompleted?.call(.failure(error)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reportWillDownloadImage(url: context.url, request: context.request)
|
||||
sessionTask.resume()
|
||||
return downloadTask
|
||||
}
|
||||
|
||||
// MARK: Downloading Task
|
||||
/// Downloads an image with a URL and option. Invoked internally by Kingfisher. Subclasses must invoke super.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: Target URL.
|
||||
/// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
|
||||
/// - completionHandler: Called when the download progress finishes. This block will be called in the queue
|
||||
/// defined in `.callbackQueue` in `options` parameter.
|
||||
/// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
|
||||
@discardableResult
|
||||
open func downloadImage(
|
||||
with url: URL,
|
||||
options: KingfisherParsedOptionsInfo,
|
||||
completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
var downloadTask: DownloadTask?
|
||||
createDownloadContext(with: url, options: options) { result in
|
||||
switch result {
|
||||
case .success(let context):
|
||||
// `downloadTask` will be set if the downloading started immediately. This is the case when no request
|
||||
// modifier or a sync modifier (`ImageDownloadRequestModifier`) is used. Otherwise, when an
|
||||
// `AsyncImageDownloadRequestModifier` is used the returned `downloadTask` of this method will be `nil`
|
||||
// and the actual "delayed" task is given in `AsyncImageDownloadRequestModifier.onDownloadTaskStarted`
|
||||
// callback.
|
||||
downloadTask = self.startDownloadTask(
|
||||
context: context,
|
||||
callback: self.createTaskCallback(completionHandler, options: options)
|
||||
)
|
||||
if let modifier = options.requestModifier {
|
||||
modifier.onDownloadTaskStarted?(downloadTask)
|
||||
}
|
||||
case .failure(let error):
|
||||
options.callbackQueue.execute {
|
||||
completionHandler?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return downloadTask
|
||||
}
|
||||
|
||||
/// Downloads an image with a URL and option.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: Target URL.
|
||||
/// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
|
||||
/// - progressBlock: Called when the download progress updated. This block will be always be called in main queue.
|
||||
/// - completionHandler: Called when the download progress finishes. This block will be called in the queue
|
||||
/// defined in `.callbackQueue` in `options` parameter.
|
||||
/// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
|
||||
@discardableResult
|
||||
open func downloadImage(
|
||||
with url: URL,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: DownloadProgressBlock? = nil,
|
||||
completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
var info = KingfisherParsedOptionsInfo(options)
|
||||
if let block = progressBlock {
|
||||
info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
|
||||
}
|
||||
return downloadImage(
|
||||
with: url,
|
||||
options: info,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/// Downloads an image with a URL and option.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: Target URL.
|
||||
/// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
|
||||
/// - completionHandler: Called when the download progress finishes. This block will be called in the queue
|
||||
/// defined in `.callbackQueue` in `options` parameter.
|
||||
/// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
|
||||
@discardableResult
|
||||
open func downloadImage(
|
||||
with url: URL,
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
|
||||
{
|
||||
downloadImage(
|
||||
with: url,
|
||||
options: KingfisherParsedOptionsInfo(options),
|
||||
completionHandler: completionHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Cancelling Task
|
||||
extension ImageDownloader {
|
||||
|
||||
/// Cancel all downloading tasks for this `ImageDownloader`. It will trigger the completion handlers
|
||||
/// for all not-yet-finished downloading tasks.
|
||||
///
|
||||
/// If you need to only cancel a certain task, call `cancel()` on the `DownloadTask`
|
||||
/// returned by the downloading methods. If you need to cancel all `DownloadTask`s of a certain url,
|
||||
/// use `ImageDownloader.cancel(url:)`.
|
||||
public func cancelAll() {
|
||||
sessionDelegate.cancelAll()
|
||||
}
|
||||
|
||||
/// Cancel all downloading tasks for a given URL. It will trigger the completion handlers for
|
||||
/// all not-yet-finished downloading tasks for the URL.
|
||||
///
|
||||
/// - Parameter url: The URL which you want to cancel downloading.
|
||||
public func cancel(url: URL) {
|
||||
sessionDelegate.cancel(url: url)
|
||||
}
|
||||
}
|
||||
|
||||
// Use the default implementation from extension of `AuthenticationChallengeResponsible`.
|
||||
extension ImageDownloader: AuthenticationChallengeResponsible {}
|
||||
|
||||
// Use the default implementation from extension of `ImageDownloaderDelegate`.
|
||||
extension ImageDownloader: ImageDownloaderDelegate {}
|
||||
|
||||
extension ImageDownloader {
|
||||
struct DownloadingContext {
|
||||
let url: URL
|
||||
let request: URLRequest
|
||||
let options: KingfisherParsedOptionsInfo
|
||||
}
|
||||
}
|
||||
184
Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift
generated
Normal file
184
Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift
generated
Normal file
@@ -0,0 +1,184 @@
|
||||
//
|
||||
// ImageDownloaderDelegate.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 2018/10/11.
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Protocol of `ImageDownloader`. This protocol provides a set of methods which are related to image downloader
|
||||
/// working stages and rules.
|
||||
public protocol ImageDownloaderDelegate: AnyObject {
|
||||
|
||||
/// Called when the `ImageDownloader` object will start downloading an image from a specified URL.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - downloader: The `ImageDownloader` object which is used for the downloading operation.
|
||||
/// - url: URL of the starting request.
|
||||
/// - request: The request object for the download process.
|
||||
///
|
||||
func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?)
|
||||
|
||||
/// Called when the `ImageDownloader` completes a downloading request with success or failure.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - downloader: The `ImageDownloader` object which is used for the downloading operation.
|
||||
/// - url: URL of the original request URL.
|
||||
/// - response: The response object of the downloading process.
|
||||
/// - error: The error in case of failure.
|
||||
///
|
||||
func imageDownloader(
|
||||
_ downloader: ImageDownloader,
|
||||
didFinishDownloadingImageForURL url: URL,
|
||||
with response: URLResponse?,
|
||||
error: Error?)
|
||||
|
||||
/// Called when the `ImageDownloader` object successfully downloaded image data from specified URL. This is
|
||||
/// your last chance to verify or modify the downloaded data before Kingfisher tries to perform addition
|
||||
/// processing on the image data.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - downloader: The `ImageDownloader` object which is used for the downloading operation.
|
||||
/// - data: The original downloaded data.
|
||||
/// - dataTask: The data task contains request and response information of the download.
|
||||
/// - Note:
|
||||
/// This can be used to pre-process raw image data before creation of `Image` instance (i.e.
|
||||
/// decrypting or verification). If `nil` returned, the processing is interrupted and a `KingfisherError` with
|
||||
/// `ResponseErrorReason.dataModifyingFailed` will be raised. You could use this fact to stop the image
|
||||
/// processing flow if you find the data is corrupted or malformed.
|
||||
///
|
||||
/// If this method is implemented, `imageDownloader(_:didDownload:for:)` will not be called anymore.
|
||||
func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with dataTask: SessionDataTask) -> Data?
|
||||
|
||||
/// Called when the `ImageDownloader` object successfully downloaded image data from specified URL. This is
|
||||
/// your last chance to verify or modify the downloaded data before Kingfisher tries to perform addition
|
||||
/// processing on the image data.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - downloader: The `ImageDownloader` object which is used for the downloading operation.
|
||||
/// - data: The original downloaded data.
|
||||
/// - url: The URL of the original request URL.
|
||||
/// - Returns: The data from which Kingfisher should use to create an image. You need to provide valid data
|
||||
/// which content is one of the supported image file format. Kingfisher will perform process on this
|
||||
/// data and try to convert it to an image object.
|
||||
/// - Note:
|
||||
/// This can be used to pre-process raw image data before creation of `Image` instance (i.e.
|
||||
/// decrypting or verification). If `nil` returned, the processing is interrupted and a `KingfisherError` with
|
||||
/// `ResponseErrorReason.dataModifyingFailed` will be raised. You could use this fact to stop the image
|
||||
/// processing flow if you find the data is corrupted or malformed.
|
||||
///
|
||||
/// If `imageDownloader(_:didDownload:with:)` is implemented, this method will not be called anymore.
|
||||
func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data?
|
||||
|
||||
/// Called when the `ImageDownloader` object successfully downloads and processes an image from specified URL.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - downloader: The `ImageDownloader` object which is used for the downloading operation.
|
||||
/// - image: The downloaded and processed image.
|
||||
/// - url: URL of the original request URL.
|
||||
/// - response: The original response object of the downloading process.
|
||||
///
|
||||
func imageDownloader(
|
||||
_ downloader: ImageDownloader,
|
||||
didDownload image: KFCrossPlatformImage,
|
||||
for url: URL,
|
||||
with response: URLResponse?)
|
||||
|
||||
/// Checks if a received HTTP status code is valid or not.
|
||||
/// By default, a status code in range 200..<400 is considered as valid.
|
||||
/// If an invalid code is received, the downloader will raise an `KingfisherError` with
|
||||
/// `ResponseErrorReason.invalidHTTPStatusCode` as its reason.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - code: The received HTTP status code.
|
||||
/// - downloader: The `ImageDownloader` object asks for validate status code.
|
||||
/// - Returns: Returns a value to indicate whether this HTTP status code is valid or not.
|
||||
/// - Note: If the default 200 to 400 valid code does not suit your need,
|
||||
/// you can implement this method to change that behavior.
|
||||
func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool
|
||||
|
||||
/// Called when the task has received a valid HTTP response after it passes other checks such as the status code.
|
||||
/// You can perform additional checks or verification on the response to determine if the download should be allowed.
|
||||
///
|
||||
/// For example, it is useful if you want to verify some header values in the response before actually starting the
|
||||
/// download.
|
||||
///
|
||||
/// If implemented, it is your responsibility to call the `completionHandler` with a proper response disposition,
|
||||
/// such as `.allow` to start the actual downloading or `.cancel` to cancel the task. If `.cancel` is used as the
|
||||
/// disposition, the downloader will raise an `KingfisherError` with
|
||||
/// `ResponseErrorReason.cancelledByDelegate` as its reason. If not implemented, any response which passes other
|
||||
/// checked will be allowed and the download starts.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - downloader: The `ImageDownloader` object which is used for the downloading operation.
|
||||
/// - response: The original response object of the downloading process.
|
||||
/// - completionHandler: A completion handler that receives the disposition for the download task. You must call
|
||||
/// this handler with either `.allow` or `.cancel`.
|
||||
func imageDownloader(
|
||||
_ downloader: ImageDownloader,
|
||||
didReceive response: URLResponse,
|
||||
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
|
||||
}
|
||||
|
||||
// Default implementation for `ImageDownloaderDelegate`.
|
||||
extension ImageDownloaderDelegate {
|
||||
public func imageDownloader(
|
||||
_ downloader: ImageDownloader,
|
||||
willDownloadImageForURL url: URL,
|
||||
with request: URLRequest?) {}
|
||||
|
||||
public func imageDownloader(
|
||||
_ downloader: ImageDownloader,
|
||||
didFinishDownloadingImageForURL url: URL,
|
||||
with response: URLResponse?,
|
||||
error: Error?) {}
|
||||
|
||||
public func imageDownloader(
|
||||
_ downloader: ImageDownloader,
|
||||
didDownload image: KFCrossPlatformImage,
|
||||
for url: URL,
|
||||
with response: URLResponse?) {}
|
||||
|
||||
public func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool {
|
||||
return (200..<400).contains(code)
|
||||
}
|
||||
|
||||
public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with task: SessionDataTask) -> Data? {
|
||||
guard let url = task.originalURL else {
|
||||
return data
|
||||
}
|
||||
return imageDownloader(downloader, didDownload: data, for: url)
|
||||
}
|
||||
|
||||
public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? {
|
||||
return data
|
||||
}
|
||||
|
||||
public func imageDownloader(
|
||||
_ downloader: ImageDownloader,
|
||||
didReceive response: URLResponse,
|
||||
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
|
||||
completionHandler(.allow)
|
||||
}
|
||||
|
||||
}
|
||||
116
Pods/Kingfisher/Sources/Networking/ImageModifier.swift
generated
Normal file
116
Pods/Kingfisher/Sources/Networking/ImageModifier.swift
generated
Normal file
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// ImageModifier.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Ethan Gill on 2017/11/28.
|
||||
//
|
||||
// Copyright (c) 2019 Ethan Gill <ethan.gill@me.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
|
||||
|
||||
/// An `ImageModifier` can be used to change properties on an image between cache serialization and the actual use of
|
||||
/// the image. The `modify(_:)` method will be called after the image retrieved from its source and before it returned
|
||||
/// to the caller. This modified image is expected to be only used for rendering purpose, any changes applied by the
|
||||
/// `ImageModifier` will not be serialized or cached.
|
||||
public protocol ImageModifier {
|
||||
/// Modify an input `Image`.
|
||||
///
|
||||
/// - parameter image: Image which will be modified by `self`
|
||||
///
|
||||
/// - returns: The modified image.
|
||||
///
|
||||
/// - Note: The return value will be unmodified if modifying is not possible on
|
||||
/// the current platform.
|
||||
/// - Note: Most modifiers support UIImage or NSImage, but not CGImage.
|
||||
func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage
|
||||
}
|
||||
|
||||
/// A wrapper for creating an `ImageModifier` easier.
|
||||
/// This type conforms to `ImageModifier` and wraps an image modify block.
|
||||
/// If the `block` throws an error, the original image will be used.
|
||||
public struct AnyImageModifier: ImageModifier {
|
||||
|
||||
/// A block which modifies images, or returns the original image
|
||||
/// if modification cannot be performed with an error.
|
||||
let block: (KFCrossPlatformImage) throws -> KFCrossPlatformImage
|
||||
|
||||
/// Creates an `AnyImageModifier` with a given `modify` block.
|
||||
public init(modify: @escaping (KFCrossPlatformImage) throws -> KFCrossPlatformImage) {
|
||||
block = modify
|
||||
}
|
||||
|
||||
/// Modify an input `Image`. See `ImageModifier` protocol for more.
|
||||
public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage {
|
||||
return (try? block(image)) ?? image
|
||||
}
|
||||
}
|
||||
|
||||
#if os(iOS) || os(tvOS) || os(watchOS)
|
||||
import UIKit
|
||||
|
||||
/// Modifier for setting the rendering mode of images.
|
||||
public struct RenderingModeImageModifier: ImageModifier {
|
||||
|
||||
/// The rendering mode to apply to the image.
|
||||
public let renderingMode: UIImage.RenderingMode
|
||||
|
||||
/// Creates a `RenderingModeImageModifier`.
|
||||
///
|
||||
/// - Parameter renderingMode: The rendering mode to apply to the image. Default is `.automatic`.
|
||||
public init(renderingMode: UIImage.RenderingMode = .automatic) {
|
||||
self.renderingMode = renderingMode
|
||||
}
|
||||
|
||||
/// Modify an input `Image`. See `ImageModifier` protocol for more.
|
||||
public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage {
|
||||
return image.withRenderingMode(renderingMode)
|
||||
}
|
||||
}
|
||||
|
||||
/// Modifier for setting the `flipsForRightToLeftLayoutDirection` property of images.
|
||||
public struct FlipsForRightToLeftLayoutDirectionImageModifier: ImageModifier {
|
||||
|
||||
/// Creates a `FlipsForRightToLeftLayoutDirectionImageModifier`.
|
||||
public init() {}
|
||||
|
||||
/// Modify an input `Image`. See `ImageModifier` protocol for more.
|
||||
public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage {
|
||||
return image.imageFlippedForRightToLeftLayoutDirection()
|
||||
}
|
||||
}
|
||||
|
||||
/// Modifier for setting the `alignmentRectInsets` property of images.
|
||||
public struct AlignmentRectInsetsImageModifier: ImageModifier {
|
||||
|
||||
/// The alignment insets to apply to the image
|
||||
public let alignmentInsets: UIEdgeInsets
|
||||
|
||||
/// Creates an `AlignmentRectInsetsImageModifier`.
|
||||
public init(alignmentInsets: UIEdgeInsets) {
|
||||
self.alignmentInsets = alignmentInsets
|
||||
}
|
||||
|
||||
/// Modify an input `Image`. See `ImageModifier` protocol for more.
|
||||
public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage {
|
||||
return image.withAlignmentRectInsets(alignmentInsets)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
442
Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift
generated
Normal file
442
Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift
generated
Normal file
@@ -0,0 +1,442 @@
|
||||
//
|
||||
// ImagePrefetcher.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Claire Knight <claire.knight@moggytech.co.uk> on 24/02/2016
|
||||
//
|
||||
// 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.
|
||||
|
||||
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
/// Progress update block of prefetcher when initialized with a list of resources.
|
||||
///
|
||||
/// - `skippedResources`: An array of resources that are already cached before the prefetching starting.
|
||||
/// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while
|
||||
/// downloading, encountered an error when downloading or the download not being started at all.
|
||||
/// - `completedResources`: An array of resources that are downloaded and cached successfully.
|
||||
public typealias PrefetcherProgressBlock =
|
||||
((_ skippedResources: [Resource], _ failedResources: [Resource], _ completedResources: [Resource]) -> Void)
|
||||
|
||||
/// Progress update block of prefetcher when initialized with a list of resources.
|
||||
///
|
||||
/// - `skippedSources`: An array of sources that are already cached before the prefetching starting.
|
||||
/// - `failedSources`: An array of sources that fail to be fetched.
|
||||
/// - `completedResources`: An array of sources that are fetched and cached successfully.
|
||||
public typealias PrefetcherSourceProgressBlock =
|
||||
((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void)
|
||||
|
||||
/// Completion block of prefetcher when initialized with a list of sources.
|
||||
///
|
||||
/// - `skippedResources`: An array of resources that are already cached before the prefetching starting.
|
||||
/// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while
|
||||
/// downloading, encountered an error when downloading or the download not being started at all.
|
||||
/// - `completedResources`: An array of resources that are downloaded and cached successfully.
|
||||
public typealias PrefetcherCompletionHandler =
|
||||
((_ skippedResources: [Resource], _ failedResources: [Resource], _ completedResources: [Resource]) -> Void)
|
||||
|
||||
/// Completion block of prefetcher when initialized with a list of sources.
|
||||
///
|
||||
/// - `skippedSources`: An array of sources that are already cached before the prefetching starting.
|
||||
/// - `failedSources`: An array of sources that fail to be fetched.
|
||||
/// - `completedSources`: An array of sources that are fetched and cached successfully.
|
||||
public typealias PrefetcherSourceCompletionHandler =
|
||||
((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void)
|
||||
|
||||
/// `ImagePrefetcher` represents a downloading manager for requesting many images via URLs, then caching them.
|
||||
/// This is useful when you know a list of image resources and want to download them before showing. It also works with
|
||||
/// some Cocoa prefetching mechanism like table view or collection view `prefetchDataSource`, to start image downloading
|
||||
/// and caching before they display on screen.
|
||||
public class ImagePrefetcher: CustomStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
return "\(Unmanaged.passUnretained(self).toOpaque())"
|
||||
}
|
||||
|
||||
/// The maximum concurrent downloads to use when prefetching images. Default is 5.
|
||||
public var maxConcurrentDownloads = 5
|
||||
|
||||
private let prefetchSources: [Source]
|
||||
private let optionsInfo: KingfisherParsedOptionsInfo
|
||||
|
||||
private var progressBlock: PrefetcherProgressBlock?
|
||||
private var completionHandler: PrefetcherCompletionHandler?
|
||||
|
||||
private var progressSourceBlock: PrefetcherSourceProgressBlock?
|
||||
private var completionSourceHandler: PrefetcherSourceCompletionHandler?
|
||||
|
||||
private var tasks = [String: DownloadTask.WrappedTask]()
|
||||
|
||||
private var pendingSources: ArraySlice<Source>
|
||||
private var skippedSources = [Source]()
|
||||
private var completedSources = [Source]()
|
||||
private var failedSources = [Source]()
|
||||
|
||||
private var stopped = false
|
||||
|
||||
// A manager used for prefetching. We will use the helper methods in manager.
|
||||
private let manager: KingfisherManager
|
||||
|
||||
private let pretchQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.pretchQueue")
|
||||
private static let requestingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.requestingQueue")
|
||||
|
||||
private var finished: Bool {
|
||||
let totalFinished: Int = failedSources.count + skippedSources.count + completedSources.count
|
||||
return totalFinished == prefetchSources.count && tasks.isEmpty
|
||||
}
|
||||
|
||||
/// Creates an image prefetcher with an array of URLs.
|
||||
///
|
||||
/// The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable.
|
||||
/// After you get a valid `ImagePrefetcher` object, you call `start()` on it to begin the prefetching process.
|
||||
/// The images which are already cached will be skipped without downloading again.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - urls: The URLs which should be prefetched.
|
||||
/// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called every time an resource is downloaded, skipped or cancelled.
|
||||
/// - completionHandler: Called when the whole prefetching process finished.
|
||||
///
|
||||
/// - Note:
|
||||
/// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
|
||||
/// the downloader and cache target respectively. You can specify another downloader or cache by using
|
||||
/// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
|
||||
/// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
|
||||
public convenience init(
|
||||
urls: [URL],
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: PrefetcherProgressBlock? = nil,
|
||||
completionHandler: PrefetcherCompletionHandler? = nil)
|
||||
{
|
||||
let resources: [Resource] = urls.map { $0 }
|
||||
self.init(
|
||||
resources: resources,
|
||||
options: options,
|
||||
progressBlock: progressBlock,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/// Creates an image prefetcher with an array of URLs.
|
||||
///
|
||||
/// The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable.
|
||||
/// After you get a valid `ImagePrefetcher` object, you call `start()` on it to begin the prefetching process.
|
||||
/// The images which are already cached will be skipped without downloading again.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - urls: The URLs which should be prefetched.
|
||||
/// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - completionHandler: Called when the whole prefetching process finished.
|
||||
///
|
||||
/// - Note:
|
||||
/// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
|
||||
/// the downloader and cache target respectively. You can specify another downloader or cache by using
|
||||
/// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
|
||||
/// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
|
||||
public convenience init(
|
||||
urls: [URL],
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
completionHandler: PrefetcherCompletionHandler? = nil)
|
||||
{
|
||||
let resources: [Resource] = urls.map { $0 }
|
||||
self.init(
|
||||
resources: resources,
|
||||
options: options,
|
||||
progressBlock: nil,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
/// Creates an image prefetcher with an array of resources.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - resources: The resources which should be prefetched. See `Resource` type for more.
|
||||
/// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called every time an resource is downloaded, skipped or cancelled.
|
||||
/// - completionHandler: Called when the whole prefetching process finished.
|
||||
///
|
||||
/// - Note:
|
||||
/// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
|
||||
/// the downloader and cache target respectively. You can specify another downloader or cache by using
|
||||
/// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
|
||||
/// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
|
||||
public convenience init(
|
||||
resources: [Resource],
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: PrefetcherProgressBlock? = nil,
|
||||
completionHandler: PrefetcherCompletionHandler? = nil)
|
||||
{
|
||||
self.init(sources: resources.map { $0.convertToSource() }, options: options)
|
||||
self.progressBlock = progressBlock
|
||||
self.completionHandler = completionHandler
|
||||
}
|
||||
|
||||
/// Creates an image prefetcher with an array of resources.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - resources: The resources which should be prefetched. See `Resource` type for more.
|
||||
/// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - completionHandler: Called when the whole prefetching process finished.
|
||||
///
|
||||
/// - Note:
|
||||
/// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
|
||||
/// the downloader and cache target respectively. You can specify another downloader or cache by using
|
||||
/// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
|
||||
/// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
|
||||
public convenience init(
|
||||
resources: [Resource],
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
completionHandler: PrefetcherCompletionHandler? = nil)
|
||||
{
|
||||
self.init(sources: resources.map { $0.convertToSource() }, options: options)
|
||||
self.completionHandler = completionHandler
|
||||
}
|
||||
|
||||
/// Creates an image prefetcher with an array of sources.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - sources: The sources which should be prefetched. See `Source` type for more.
|
||||
/// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - progressBlock: Called every time an source fetching successes, fails, is skipped.
|
||||
/// - completionHandler: Called when the whole prefetching process finished.
|
||||
///
|
||||
/// - Note:
|
||||
/// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
|
||||
/// the downloader and cache target respectively. You can specify another downloader or cache by using
|
||||
/// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
|
||||
/// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
|
||||
public convenience init(sources: [Source],
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
progressBlock: PrefetcherSourceProgressBlock? = nil,
|
||||
completionHandler: PrefetcherSourceCompletionHandler? = nil)
|
||||
{
|
||||
self.init(sources: sources, options: options)
|
||||
self.progressSourceBlock = progressBlock
|
||||
self.completionSourceHandler = completionHandler
|
||||
}
|
||||
|
||||
/// Creates an image prefetcher with an array of sources.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - sources: The sources which should be prefetched. See `Source` type for more.
|
||||
/// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more.
|
||||
/// - completionHandler: Called when the whole prefetching process finished.
|
||||
///
|
||||
/// - Note:
|
||||
/// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as
|
||||
/// the downloader and cache target respectively. You can specify another downloader or cache by using
|
||||
/// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in
|
||||
/// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method.
|
||||
public convenience init(sources: [Source],
|
||||
options: KingfisherOptionsInfo? = nil,
|
||||
completionHandler: PrefetcherSourceCompletionHandler? = nil)
|
||||
{
|
||||
self.init(sources: sources, options: options)
|
||||
self.completionSourceHandler = completionHandler
|
||||
}
|
||||
|
||||
init(sources: [Source], options: KingfisherOptionsInfo?) {
|
||||
var options = KingfisherParsedOptionsInfo(options)
|
||||
prefetchSources = sources
|
||||
pendingSources = ArraySlice(sources)
|
||||
|
||||
// We want all callbacks from our prefetch queue, so we should ignore the callback queue in options.
|
||||
// Add our own callback dispatch queue to make sure all internal callbacks are
|
||||
// coming back in our expected queue.
|
||||
options.callbackQueue = .dispatch(pretchQueue)
|
||||
optionsInfo = options
|
||||
|
||||
let cache = optionsInfo.targetCache ?? .default
|
||||
let downloader = optionsInfo.downloader ?? .default
|
||||
manager = KingfisherManager(downloader: downloader, cache: cache)
|
||||
}
|
||||
|
||||
/// Starts to download the resources and cache them. This can be useful for background downloading
|
||||
/// of assets that are required for later use in an app. This code will not try and update any UI
|
||||
/// with the results of the process.
|
||||
public func start() {
|
||||
pretchQueue.async {
|
||||
guard !self.stopped else {
|
||||
assertionFailure("You can not restart the same prefetcher. Try to create a new prefetcher.")
|
||||
self.handleComplete()
|
||||
return
|
||||
}
|
||||
|
||||
guard self.maxConcurrentDownloads > 0 else {
|
||||
assertionFailure("There should be concurrent downloads value should be at least 1.")
|
||||
self.handleComplete()
|
||||
return
|
||||
}
|
||||
|
||||
// Empty case.
|
||||
guard self.prefetchSources.count > 0 else {
|
||||
self.handleComplete()
|
||||
return
|
||||
}
|
||||
|
||||
let initialConcurrentDownloads = min(self.prefetchSources.count, self.maxConcurrentDownloads)
|
||||
for _ in 0 ..< initialConcurrentDownloads {
|
||||
if let resource = self.pendingSources.popFirst() {
|
||||
self.startPrefetching(resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops current downloading progress, and cancel any future prefetching activity that might be occuring.
|
||||
public func stop() {
|
||||
pretchQueue.async {
|
||||
if self.finished { return }
|
||||
self.stopped = true
|
||||
self.tasks.values.forEach { $0.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
private func downloadAndCache(_ source: Source) {
|
||||
|
||||
let downloadTaskCompletionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void) = { result in
|
||||
self.tasks.removeValue(forKey: source.cacheKey)
|
||||
do {
|
||||
let _ = try result.get()
|
||||
self.completedSources.append(source)
|
||||
} catch {
|
||||
self.failedSources.append(source)
|
||||
}
|
||||
|
||||
self.reportProgress()
|
||||
if self.stopped {
|
||||
if self.tasks.isEmpty {
|
||||
self.failedSources.append(contentsOf: self.pendingSources)
|
||||
self.handleComplete()
|
||||
}
|
||||
} else {
|
||||
self.reportCompletionOrStartNext()
|
||||
}
|
||||
}
|
||||
|
||||
var downloadTask: DownloadTask.WrappedTask?
|
||||
ImagePrefetcher.requestingQueue.sync {
|
||||
let context = RetrievingContext(
|
||||
options: optionsInfo, originalSource: source
|
||||
)
|
||||
downloadTask = manager.loadAndCacheImage(
|
||||
source: source,
|
||||
context: context,
|
||||
completionHandler: downloadTaskCompletionHandler)
|
||||
}
|
||||
|
||||
if let downloadTask = downloadTask {
|
||||
tasks[source.cacheKey] = downloadTask
|
||||
}
|
||||
}
|
||||
|
||||
private func append(cached source: Source) {
|
||||
skippedSources.append(source)
|
||||
|
||||
reportProgress()
|
||||
reportCompletionOrStartNext()
|
||||
}
|
||||
|
||||
private func startPrefetching(_ source: Source)
|
||||
{
|
||||
if optionsInfo.forceRefresh {
|
||||
downloadAndCache(source)
|
||||
return
|
||||
}
|
||||
|
||||
let cacheType = manager.cache.imageCachedType(
|
||||
forKey: source.cacheKey,
|
||||
processorIdentifier: optionsInfo.processor.identifier)
|
||||
switch cacheType {
|
||||
case .memory:
|
||||
append(cached: source)
|
||||
case .disk:
|
||||
if optionsInfo.alsoPrefetchToMemory {
|
||||
let context = RetrievingContext(options: optionsInfo, originalSource: source)
|
||||
_ = manager.retrieveImageFromCache(
|
||||
source: source,
|
||||
context: context)
|
||||
{
|
||||
_ in
|
||||
self.append(cached: source)
|
||||
}
|
||||
} else {
|
||||
append(cached: source)
|
||||
}
|
||||
case .none:
|
||||
downloadAndCache(source)
|
||||
}
|
||||
}
|
||||
|
||||
private func reportProgress() {
|
||||
|
||||
if progressBlock == nil && progressSourceBlock == nil {
|
||||
return
|
||||
}
|
||||
|
||||
let skipped = self.skippedSources
|
||||
let failed = self.failedSources
|
||||
let completed = self.completedSources
|
||||
CallbackQueue.mainCurrentOrAsync.execute {
|
||||
self.progressSourceBlock?(skipped, failed, completed)
|
||||
self.progressBlock?(
|
||||
skipped.compactMap { $0.asResource },
|
||||
failed.compactMap { $0.asResource },
|
||||
completed.compactMap { $0.asResource }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func reportCompletionOrStartNext() {
|
||||
if let resource = self.pendingSources.popFirst() {
|
||||
// Loose call stack for huge ammount of sources.
|
||||
pretchQueue.async { self.startPrefetching(resource) }
|
||||
} else {
|
||||
guard allFinished else { return }
|
||||
self.handleComplete()
|
||||
}
|
||||
}
|
||||
|
||||
var allFinished: Bool {
|
||||
return skippedSources.count + failedSources.count + completedSources.count == prefetchSources.count
|
||||
}
|
||||
|
||||
private func handleComplete() {
|
||||
|
||||
if completionHandler == nil && completionSourceHandler == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// The completion handler should be called on the main thread
|
||||
CallbackQueue.mainCurrentOrAsync.execute {
|
||||
self.completionSourceHandler?(self.skippedSources, self.failedSources, self.completedSources)
|
||||
self.completionHandler?(
|
||||
self.skippedSources.compactMap { $0.asResource },
|
||||
self.failedSources.compactMap { $0.asResource },
|
||||
self.completedSources.compactMap { $0.asResource }
|
||||
)
|
||||
self.completionHandler = nil
|
||||
self.progressBlock = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
76
Pods/Kingfisher/Sources/Networking/RedirectHandler.swift
generated
Normal file
76
Pods/Kingfisher/Sources/Networking/RedirectHandler.swift
generated
Normal file
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// RedirectHandler.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Roman Maidanovych on 2018/12/10.
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Represents and wraps a method for modifying request during an image download request redirection.
|
||||
public protocol ImageDownloadRedirectHandler {
|
||||
|
||||
/// The `ImageDownloadRedirectHandler` contained will be used to change the request before redirection.
|
||||
/// This is the posibility you can modify the image download request during redirection. You can modify the
|
||||
/// request for some customizing purpose, such as adding auth token to the header, do basic HTTP auth or
|
||||
/// something like url mapping.
|
||||
///
|
||||
/// Usually, you pass an `ImageDownloadRedirectHandler` as the associated value of
|
||||
/// `KingfisherOptionsInfoItem.redirectHandler` and use it as the `options` parameter in related methods.
|
||||
///
|
||||
/// If you do nothing with the input `request` and return it as is, a downloading process will redirect with it.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - task: The current `SessionDataTask` which triggers this redirect.
|
||||
/// - response: The response received during redirection.
|
||||
/// - newRequest: The request for redirection which can be modified.
|
||||
/// - completionHandler: A closure for being called with modified request.
|
||||
func handleHTTPRedirection(
|
||||
for task: SessionDataTask,
|
||||
response: HTTPURLResponse,
|
||||
newRequest: URLRequest,
|
||||
completionHandler: @escaping (URLRequest?) -> Void)
|
||||
}
|
||||
|
||||
/// A wrapper for creating an `ImageDownloadRedirectHandler` easier.
|
||||
/// This type conforms to `ImageDownloadRedirectHandler` and wraps a redirect request modify block.
|
||||
public struct AnyRedirectHandler: ImageDownloadRedirectHandler {
|
||||
|
||||
let block: (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void
|
||||
|
||||
public func handleHTTPRedirection(
|
||||
for task: SessionDataTask,
|
||||
response: HTTPURLResponse,
|
||||
newRequest: URLRequest,
|
||||
completionHandler: @escaping (URLRequest?) -> Void)
|
||||
{
|
||||
block(task, response, newRequest, completionHandler)
|
||||
}
|
||||
|
||||
/// Creates a value of `ImageDownloadRedirectHandler` which runs `modify` block.
|
||||
///
|
||||
/// - Parameter modify: The request modifying block runs when a request modifying task comes.
|
||||
///
|
||||
public init(handle: @escaping (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (URLRequest?) -> Void) -> Void) {
|
||||
block = handle
|
||||
}
|
||||
}
|
||||
108
Pods/Kingfisher/Sources/Networking/RequestModifier.swift
generated
Normal file
108
Pods/Kingfisher/Sources/Networking/RequestModifier.swift
generated
Normal file
@@ -0,0 +1,108 @@
|
||||
//
|
||||
// RequestModifier.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 2016/09/05.
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Represents and wraps a method for modifying request before an image download request starts in an asynchronous way.
|
||||
public protocol AsyncImageDownloadRequestModifier {
|
||||
|
||||
/// This method will be called just before the `request` being sent.
|
||||
/// This is the last chance you can modify the image download request. You can modify the request for some
|
||||
/// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping.
|
||||
/// When you have done with the modification, call the `reportModified` block with the modified request and the data
|
||||
/// download will happen with this request.
|
||||
///
|
||||
/// Usually, you pass an `AsyncImageDownloadRequestModifier` as the associated value of
|
||||
/// `KingfisherOptionsInfoItem.requestModifier` and use it as the `options` parameter in related methods.
|
||||
///
|
||||
/// If you do nothing with the input `request` and return it as is, a downloading process will start with it.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - request: The input request contains necessary information like `url`. This request is generated
|
||||
/// according to your resource url as a GET request.
|
||||
/// - reportModified: The callback block you need to call after the asynchronous modifying done.
|
||||
///
|
||||
func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void)
|
||||
|
||||
/// A block will be called when the download task started.
|
||||
///
|
||||
/// If an `AsyncImageDownloadRequestModifier` and the asynchronous modification happens before the download, the
|
||||
/// related download method will not return a valid `DownloadTask` value. Instead, you can get one from this method.
|
||||
var onDownloadTaskStarted: ((DownloadTask?) -> Void)? { get }
|
||||
}
|
||||
|
||||
/// Represents and wraps a method for modifying request before an image download request starts.
|
||||
public protocol ImageDownloadRequestModifier: AsyncImageDownloadRequestModifier {
|
||||
|
||||
/// This method will be called just before the `request` being sent.
|
||||
/// This is the last chance you can modify the image download request. You can modify the request for some
|
||||
/// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping.
|
||||
///
|
||||
/// Usually, you pass an `ImageDownloadRequestModifier` as the associated value of
|
||||
/// `KingfisherOptionsInfoItem.requestModifier` and use it as the `options` parameter in related methods.
|
||||
///
|
||||
/// If you do nothing with the input `request` and return it as is, a downloading process will start with it.
|
||||
///
|
||||
/// - Parameter request: The input request contains necessary information like `url`. This request is generated
|
||||
/// according to your resource url as a GET request.
|
||||
/// - Returns: A modified version of request, which you wish to use for downloading an image. If `nil` returned,
|
||||
/// a `KingfisherError.requestError` with `.emptyRequest` as its reason will occur.
|
||||
///
|
||||
func modified(for request: URLRequest) -> URLRequest?
|
||||
}
|
||||
|
||||
extension ImageDownloadRequestModifier {
|
||||
public func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void) {
|
||||
let request = modified(for: request)
|
||||
reportModified(request)
|
||||
}
|
||||
|
||||
/// This is `nil` for a sync `ImageDownloadRequestModifier` by default. You can get the `DownloadTask` from the
|
||||
/// return value of downloader method.
|
||||
public var onDownloadTaskStarted: ((DownloadTask?) -> Void)? { return nil }
|
||||
}
|
||||
|
||||
/// A wrapper for creating an `ImageDownloadRequestModifier` easier.
|
||||
/// This type conforms to `ImageDownloadRequestModifier` and wraps an image modify block.
|
||||
public struct AnyModifier: ImageDownloadRequestModifier {
|
||||
|
||||
let block: (URLRequest) -> URLRequest?
|
||||
|
||||
/// For `ImageDownloadRequestModifier` conformation.
|
||||
public func modified(for request: URLRequest) -> URLRequest? {
|
||||
return block(request)
|
||||
}
|
||||
|
||||
/// Creates a value of `ImageDownloadRequestModifier` which runs `modify` block.
|
||||
///
|
||||
/// - Parameter modify: The request modifying block runs when a request modifying task comes.
|
||||
/// The return `URLRequest?` value of this block will be used as the image download request.
|
||||
/// If `nil` returned, a `KingfisherError.requestError` with `.emptyRequest` as its
|
||||
/// reason will occur.
|
||||
public init(modify: @escaping (URLRequest) -> URLRequest?) {
|
||||
block = modify
|
||||
}
|
||||
}
|
||||
153
Pods/Kingfisher/Sources/Networking/RetryStrategy.swift
generated
Normal file
153
Pods/Kingfisher/Sources/Networking/RetryStrategy.swift
generated
Normal file
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// RetryStrategy.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2020/05/04.
|
||||
//
|
||||
// Copyright (c) 2020 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
|
||||
|
||||
/// Represents a retry context which could be used to determine the current retry status.
|
||||
public class RetryContext {
|
||||
|
||||
/// The source from which the target image should be retrieved.
|
||||
public let source: Source
|
||||
|
||||
/// The last error which caused current retry behavior.
|
||||
public let error: KingfisherError
|
||||
|
||||
/// The retried count before current retry happens. This value is `0` if the current retry is for the first time.
|
||||
public var retriedCount: Int
|
||||
|
||||
/// A user set value for passing any other information during the retry. If you choose to use `RetryDecision.retry`
|
||||
/// as the retry decision for `RetryStrategy.retry(context:retryHandler:)`, the associated value of
|
||||
/// `RetryDecision.retry` will be delivered to you in the next retry.
|
||||
public internal(set) var userInfo: Any? = nil
|
||||
|
||||
init(source: Source, error: KingfisherError) {
|
||||
self.source = source
|
||||
self.error = error
|
||||
self.retriedCount = 0
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func increaseRetryCount() -> RetryContext {
|
||||
retriedCount += 1
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents decision of behavior on the current retry.
|
||||
public enum RetryDecision {
|
||||
/// A retry should happen. The associated `userInfo` will be pass to the next retry in the `RetryContext` parameter.
|
||||
case retry(userInfo: Any?)
|
||||
/// There should be no more retry attempt. The image retrieving process will fail with an error.
|
||||
case stop
|
||||
}
|
||||
|
||||
/// Defines a retry strategy can be applied to a `.retryStrategy` option.
|
||||
public protocol RetryStrategy {
|
||||
|
||||
/// Kingfisher calls this method if an error happens during the image retrieving process from a `KingfisherManager`.
|
||||
/// You implement this method to provide necessary logic based on the `context` parameter. Then you need to call
|
||||
/// `retryHandler` to pass the retry decision back to Kingfisher.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - context: The retry context containing information of current retry attempt.
|
||||
/// - retryHandler: A block you need to call with a decision of whether the retry should happen or not.
|
||||
func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void)
|
||||
}
|
||||
|
||||
/// A retry strategy that guides Kingfisher to retry when a `.responseError` happens, with a specified max retry count
|
||||
/// and a certain interval mechanism.
|
||||
public struct DelayRetryStrategy: RetryStrategy {
|
||||
|
||||
/// Represents the interval mechanism which used in a `DelayRetryStrategy`.
|
||||
public enum Interval {
|
||||
/// The next retry attempt should happen in fixed seconds. For example, if the associated value is 3, the
|
||||
/// attempts happens after 3 seconds after the previous decision is made.
|
||||
case seconds(TimeInterval)
|
||||
/// The next retry attempt should happen in an accumulated duration. For example, if the associated value is 3,
|
||||
/// the attempts happens with interval of 3, 6, 9, 12, ... seconds.
|
||||
case accumulated(TimeInterval)
|
||||
/// Uses a block to determine the next interval. The current retry count is given as a parameter.
|
||||
case custom(block: (_ retriedCount: Int) -> TimeInterval)
|
||||
|
||||
func timeInterval(for retriedCount: Int) -> TimeInterval {
|
||||
let retryAfter: TimeInterval
|
||||
switch self {
|
||||
case .seconds(let interval):
|
||||
retryAfter = interval
|
||||
case .accumulated(let interval):
|
||||
retryAfter = Double(retriedCount + 1) * interval
|
||||
case .custom(let block):
|
||||
retryAfter = block(retriedCount)
|
||||
}
|
||||
return retryAfter
|
||||
}
|
||||
}
|
||||
|
||||
/// The max retry count defined for the retry strategy
|
||||
public let maxRetryCount: Int
|
||||
|
||||
/// The retry interval mechanism defined for the retry strategy.
|
||||
public let retryInterval: Interval
|
||||
|
||||
/// Creates a delay retry strategy.
|
||||
/// - Parameters:
|
||||
/// - maxRetryCount: The max retry count.
|
||||
/// - retryInterval: The retry interval mechanism. By default, `.seconds(3)` is used to provide a constant retry
|
||||
/// interval.
|
||||
public init(maxRetryCount: Int, retryInterval: Interval = .seconds(3)) {
|
||||
self.maxRetryCount = maxRetryCount
|
||||
self.retryInterval = retryInterval
|
||||
}
|
||||
|
||||
public func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void) {
|
||||
// Retry count exceeded.
|
||||
guard context.retriedCount < maxRetryCount else {
|
||||
retryHandler(.stop)
|
||||
return
|
||||
}
|
||||
|
||||
// User cancel the task. No retry.
|
||||
guard !context.error.isTaskCancelled else {
|
||||
retryHandler(.stop)
|
||||
return
|
||||
}
|
||||
|
||||
// Only retry for a response error.
|
||||
guard case KingfisherError.responseError = context.error else {
|
||||
retryHandler(.stop)
|
||||
return
|
||||
}
|
||||
|
||||
let interval = retryInterval.timeInterval(for: context.retriedCount)
|
||||
if interval == 0 {
|
||||
retryHandler(.retry(userInfo: nil))
|
||||
} else {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
|
||||
retryHandler(.retry(userInfo: nil))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
127
Pods/Kingfisher/Sources/Networking/SessionDataTask.swift
generated
Normal file
127
Pods/Kingfisher/Sources/Networking/SessionDataTask.swift
generated
Normal file
@@ -0,0 +1,127 @@
|
||||
//
|
||||
// SessionDataTask.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 2018/11/1.
|
||||
//
|
||||
// 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
|
||||
|
||||
/// Represents a session data task in `ImageDownloader`. It consists of an underlying `URLSessionDataTask` and
|
||||
/// an array of `TaskCallback`. Multiple `TaskCallback`s could be added for a single downloading data task.
|
||||
public class SessionDataTask {
|
||||
|
||||
/// Represents the type of token which used for cancelling a task.
|
||||
public typealias CancelToken = Int
|
||||
|
||||
struct TaskCallback {
|
||||
let onCompleted: Delegate<Result<ImageLoadingResult, KingfisherError>, Void>?
|
||||
let options: KingfisherParsedOptionsInfo
|
||||
}
|
||||
|
||||
/// Downloaded raw data of current task.
|
||||
public private(set) var mutableData: Data
|
||||
|
||||
// This is a copy of `task.originalRequest?.url`. It is for getting a race-safe behavior for a pitfall on iOS 13.
|
||||
// Ref: https://github.com/onevcat/Kingfisher/issues/1511
|
||||
public let originalURL: URL?
|
||||
|
||||
/// The underlying download task. It is only for debugging purpose when you encountered an error. You should not
|
||||
/// modify the content of this task or start it yourself.
|
||||
public let task: URLSessionDataTask
|
||||
private var callbacksStore = [CancelToken: TaskCallback]()
|
||||
|
||||
var callbacks: [SessionDataTask.TaskCallback] {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return Array(callbacksStore.values)
|
||||
}
|
||||
|
||||
private var currentToken = 0
|
||||
private let lock = NSLock()
|
||||
|
||||
let onTaskDone = Delegate<(Result<(Data, URLResponse?), KingfisherError>, [TaskCallback]), Void>()
|
||||
let onCallbackCancelled = Delegate<(CancelToken, TaskCallback), Void>()
|
||||
|
||||
var started = false
|
||||
var containsCallbacks: Bool {
|
||||
// We should be able to use `task.state != .running` to check it.
|
||||
// However, in some rare cases, cancelling the task does not change
|
||||
// task state to `.cancelling` immediately, but still in `.running`.
|
||||
// So we need to check callbacks count to for sure that it is safe to remove the
|
||||
// task in delegate.
|
||||
return !callbacks.isEmpty
|
||||
}
|
||||
|
||||
init(task: URLSessionDataTask) {
|
||||
self.task = task
|
||||
self.originalURL = task.originalRequest?.url
|
||||
mutableData = Data()
|
||||
}
|
||||
|
||||
func addCallback(_ callback: TaskCallback) -> CancelToken {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
callbacksStore[currentToken] = callback
|
||||
defer { currentToken += 1 }
|
||||
return currentToken
|
||||
}
|
||||
|
||||
func removeCallback(_ token: CancelToken) -> TaskCallback? {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
if let callback = callbacksStore[token] {
|
||||
callbacksStore[token] = nil
|
||||
return callback
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeAllCallbacks() -> Void {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
callbacksStore.removeAll()
|
||||
}
|
||||
|
||||
func resume() {
|
||||
guard !started else { return }
|
||||
started = true
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func cancel(token: CancelToken) {
|
||||
guard let callback = removeCallback(token) else {
|
||||
return
|
||||
}
|
||||
onCallbackCancelled.call((token, callback))
|
||||
}
|
||||
|
||||
func forceCancel() {
|
||||
for token in callbacksStore.keys {
|
||||
cancel(token: token)
|
||||
}
|
||||
}
|
||||
|
||||
func didReceiveData(_ data: Data) {
|
||||
mutableData.append(data)
|
||||
}
|
||||
}
|
||||
271
Pods/Kingfisher/Sources/Networking/SessionDelegate.swift
generated
Normal file
271
Pods/Kingfisher/Sources/Networking/SessionDelegate.swift
generated
Normal file
@@ -0,0 +1,271 @@
|
||||
//
|
||||
// SessionDelegate.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 2018/11/1.
|
||||
//
|
||||
// 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
|
||||
|
||||
// Represents the delegate object of downloader session. It also behave like a task manager for downloading.
|
||||
@objc(KFSessionDelegate) // Fix for ObjC header name conflicting. https://github.com/onevcat/Kingfisher/issues/1530
|
||||
open class SessionDelegate: NSObject {
|
||||
|
||||
typealias SessionChallengeFunc = (
|
||||
URLSession,
|
||||
URLAuthenticationChallenge,
|
||||
(URLSession.AuthChallengeDisposition, URLCredential?) -> Void
|
||||
)
|
||||
|
||||
typealias SessionTaskChallengeFunc = (
|
||||
URLSession,
|
||||
URLSessionTask,
|
||||
URLAuthenticationChallenge,
|
||||
(URLSession.AuthChallengeDisposition, URLCredential?) -> Void
|
||||
)
|
||||
|
||||
private var tasks: [URL: SessionDataTask] = [:]
|
||||
private let lock = NSLock()
|
||||
|
||||
let onValidStatusCode = Delegate<Int, Bool>()
|
||||
let onResponseReceived = Delegate<(URLResponse, (URLSession.ResponseDisposition) -> Void), Void>()
|
||||
let onDownloadingFinished = Delegate<(URL, Result<URLResponse, KingfisherError>), Void>()
|
||||
let onDidDownloadData = Delegate<SessionDataTask, Data?>()
|
||||
|
||||
let onReceiveSessionChallenge = Delegate<SessionChallengeFunc, Void>()
|
||||
let onReceiveSessionTaskChallenge = Delegate<SessionTaskChallengeFunc, Void>()
|
||||
|
||||
func add(
|
||||
_ dataTask: URLSessionDataTask,
|
||||
url: URL,
|
||||
callback: SessionDataTask.TaskCallback) -> DownloadTask
|
||||
{
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
|
||||
// Create a new task if necessary.
|
||||
let task = SessionDataTask(task: dataTask)
|
||||
task.onCallbackCancelled.delegate(on: self) { [weak task] (self, value) in
|
||||
guard let task = task else { return }
|
||||
|
||||
let (token, callback) = value
|
||||
|
||||
let error = KingfisherError.requestError(reason: .taskCancelled(task: task, token: token))
|
||||
task.onTaskDone.call((.failure(error), [callback]))
|
||||
// No other callbacks waiting, we can clear the task now.
|
||||
if !task.containsCallbacks {
|
||||
let dataTask = task.task
|
||||
|
||||
self.cancelTask(dataTask)
|
||||
self.remove(task)
|
||||
}
|
||||
}
|
||||
let token = task.addCallback(callback)
|
||||
tasks[url] = task
|
||||
return DownloadTask(sessionTask: task, cancelToken: token)
|
||||
}
|
||||
|
||||
private func cancelTask(_ dataTask: URLSessionDataTask) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
dataTask.cancel()
|
||||
}
|
||||
|
||||
func append(
|
||||
_ task: SessionDataTask,
|
||||
callback: SessionDataTask.TaskCallback) -> DownloadTask
|
||||
{
|
||||
let token = task.addCallback(callback)
|
||||
return DownloadTask(sessionTask: task, cancelToken: token)
|
||||
}
|
||||
|
||||
private func remove(_ task: SessionDataTask) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
|
||||
guard let url = task.originalURL else {
|
||||
return
|
||||
}
|
||||
task.removeAllCallbacks()
|
||||
tasks[url] = nil
|
||||
}
|
||||
|
||||
private func task(for task: URLSessionTask) -> SessionDataTask? {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
|
||||
guard let url = task.originalRequest?.url else {
|
||||
return nil
|
||||
}
|
||||
guard let sessionTask = tasks[url] else {
|
||||
return nil
|
||||
}
|
||||
guard sessionTask.task.taskIdentifier == task.taskIdentifier else {
|
||||
return nil
|
||||
}
|
||||
return sessionTask
|
||||
}
|
||||
|
||||
func task(for url: URL) -> SessionDataTask? {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return tasks[url]
|
||||
}
|
||||
|
||||
func cancelAll() {
|
||||
lock.lock()
|
||||
let taskValues = tasks.values
|
||||
lock.unlock()
|
||||
for task in taskValues {
|
||||
task.forceCancel()
|
||||
}
|
||||
}
|
||||
|
||||
func cancel(url: URL) {
|
||||
lock.lock()
|
||||
let task = tasks[url]
|
||||
lock.unlock()
|
||||
task?.forceCancel()
|
||||
}
|
||||
}
|
||||
|
||||
extension SessionDelegate: URLSessionDataDelegate {
|
||||
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
dataTask: URLSessionDataTask,
|
||||
didReceive response: URLResponse,
|
||||
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
|
||||
{
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
let error = KingfisherError.responseError(reason: .invalidURLResponse(response: response))
|
||||
onCompleted(task: dataTask, result: .failure(error))
|
||||
completionHandler(.cancel)
|
||||
return
|
||||
}
|
||||
|
||||
let httpStatusCode = httpResponse.statusCode
|
||||
guard onValidStatusCode.call(httpStatusCode) == true else {
|
||||
let error = KingfisherError.responseError(reason: .invalidHTTPStatusCode(response: httpResponse))
|
||||
onCompleted(task: dataTask, result: .failure(error))
|
||||
completionHandler(.cancel)
|
||||
return
|
||||
}
|
||||
|
||||
let inspectedHandler: (URLSession.ResponseDisposition) -> Void = { disposition in
|
||||
if disposition == .cancel {
|
||||
let error = KingfisherError.responseError(reason: .cancelledByDelegate(response: response))
|
||||
self.onCompleted(task: dataTask, result: .failure(error))
|
||||
}
|
||||
completionHandler(disposition)
|
||||
}
|
||||
onResponseReceived.call((response, inspectedHandler))
|
||||
}
|
||||
|
||||
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
||||
guard let task = self.task(for: dataTask) else {
|
||||
return
|
||||
}
|
||||
|
||||
task.didReceiveData(data)
|
||||
|
||||
task.callbacks.forEach { callback in
|
||||
callback.options.onDataReceived?.forEach { sideEffect in
|
||||
sideEffect.onDataReceived(session, task: task, data: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
guard let sessionTask = self.task(for: task) else { return }
|
||||
|
||||
if let url = sessionTask.originalURL {
|
||||
let result: Result<URLResponse, KingfisherError>
|
||||
if let error = error {
|
||||
result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error)))
|
||||
} else if let response = task.response {
|
||||
result = .success(response)
|
||||
} else {
|
||||
result = .failure(KingfisherError.responseError(reason: .noURLResponse(task: sessionTask)))
|
||||
}
|
||||
onDownloadingFinished.call((url, result))
|
||||
}
|
||||
|
||||
let result: Result<(Data, URLResponse?), KingfisherError>
|
||||
if let error = error {
|
||||
result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error)))
|
||||
} else {
|
||||
if let data = onDidDownloadData.call(sessionTask) {
|
||||
result = .success((data, task.response))
|
||||
} else {
|
||||
result = .failure(KingfisherError.responseError(reason: .dataModifyingFailed(task: sessionTask)))
|
||||
}
|
||||
}
|
||||
onCompleted(task: task, result: result)
|
||||
}
|
||||
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
|
||||
{
|
||||
onReceiveSessionChallenge.call((session, challenge, completionHandler))
|
||||
}
|
||||
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
|
||||
{
|
||||
onReceiveSessionTaskChallenge.call((session, task, challenge, completionHandler))
|
||||
}
|
||||
|
||||
open func urlSession(
|
||||
_ session: URLSession,
|
||||
task: URLSessionTask,
|
||||
willPerformHTTPRedirection response: HTTPURLResponse,
|
||||
newRequest request: URLRequest,
|
||||
completionHandler: @escaping (URLRequest?) -> Void)
|
||||
{
|
||||
guard let sessionDataTask = self.task(for: task),
|
||||
let redirectHandler = Array(sessionDataTask.callbacks).last?.options.redirectHandler else
|
||||
{
|
||||
completionHandler(request)
|
||||
return
|
||||
}
|
||||
|
||||
redirectHandler.handleHTTPRedirection(
|
||||
for: sessionDataTask,
|
||||
response: response,
|
||||
newRequest: request,
|
||||
completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
private func onCompleted(task: URLSessionTask, result: Result<(Data, URLResponse?), KingfisherError>) {
|
||||
guard let sessionTask = self.task(for: task) else {
|
||||
return
|
||||
}
|
||||
sessionTask.onTaskDone.call((result, sessionTask.callbacks))
|
||||
remove(sessionTask)
|
||||
}
|
||||
}
|
||||
149
Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift
generated
Normal file
149
Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift
generated
Normal file
@@ -0,0 +1,149 @@
|
||||
//
|
||||
// ImageBinder.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2019/06/27.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#if canImport(SwiftUI) && canImport(Combine)
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
extension KFImage {
|
||||
|
||||
/// Represents a binder for `KFImage`. It takes responsibility as an `ObjectBinding` and performs
|
||||
/// image downloading and progress reporting based on `KingfisherManager`.
|
||||
class ImageBinder: ObservableObject {
|
||||
|
||||
init() {}
|
||||
|
||||
var downloadTask: DownloadTask?
|
||||
private var loading = false
|
||||
|
||||
var loadingOrSucceeded: Bool {
|
||||
return loading || loadedImage != nil
|
||||
}
|
||||
|
||||
// Do not use @Published due to https://github.com/onevcat/Kingfisher/issues/1717. Revert to @Published once
|
||||
// we can drop iOS 12.
|
||||
private(set) var loaded = false
|
||||
|
||||
private(set) var animating = false
|
||||
|
||||
var loadedImage: KFCrossPlatformImage? = nil { willSet { objectWillChange.send() } }
|
||||
var progress: Progress = .init()
|
||||
|
||||
func markLoading() {
|
||||
loading = true
|
||||
}
|
||||
|
||||
func markLoaded(sendChangeEvent: Bool) {
|
||||
loaded = true
|
||||
if sendChangeEvent {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
func start<HoldingView: KFImageHoldingView>(context: Context<HoldingView>) {
|
||||
guard let source = context.source else {
|
||||
CallbackQueue.mainCurrentOrAsync.execute {
|
||||
context.onFailureDelegate.call(KingfisherError.imageSettingError(reason: .emptySource))
|
||||
if let image = context.options.onFailureImage {
|
||||
self.loadedImage = image
|
||||
}
|
||||
self.loading = false
|
||||
self.markLoaded(sendChangeEvent: false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
loading = true
|
||||
|
||||
progress = .init()
|
||||
downloadTask = KingfisherManager.shared
|
||||
.retrieveImage(
|
||||
with: source,
|
||||
options: context.options,
|
||||
progressBlock: { size, total in
|
||||
self.updateProgress(downloaded: size, total: total)
|
||||
context.onProgressDelegate.call((size, total))
|
||||
},
|
||||
completionHandler: { [weak self] result in
|
||||
|
||||
guard let self = self else { return }
|
||||
|
||||
CallbackQueue.mainCurrentOrAsync.execute {
|
||||
self.downloadTask = nil
|
||||
self.loading = false
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let value):
|
||||
CallbackQueue.mainCurrentOrAsync.execute {
|
||||
if let fadeDuration = context.fadeTransitionDuration(cacheType: value.cacheType) {
|
||||
self.animating = true
|
||||
let animation = Animation.linear(duration: fadeDuration)
|
||||
withAnimation(animation) {
|
||||
// Trigger the view render to apply the animation.
|
||||
self.markLoaded(sendChangeEvent: true)
|
||||
}
|
||||
} else {
|
||||
self.markLoaded(sendChangeEvent: false)
|
||||
}
|
||||
self.loadedImage = value.image
|
||||
self.animating = false
|
||||
}
|
||||
|
||||
CallbackQueue.mainAsync.execute {
|
||||
context.onSuccessDelegate.call(value)
|
||||
}
|
||||
case .failure(let error):
|
||||
CallbackQueue.mainCurrentOrAsync.execute {
|
||||
if let image = context.options.onFailureImage {
|
||||
self.loadedImage = image
|
||||
}
|
||||
self.markLoaded(sendChangeEvent: true)
|
||||
}
|
||||
|
||||
CallbackQueue.mainAsync.execute {
|
||||
context.onFailureDelegate.call(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func updateProgress(downloaded: Int64, total: Int64) {
|
||||
progress.totalUnitCount = total
|
||||
progress.completedUnitCount = downloaded
|
||||
objectWillChange.send()
|
||||
}
|
||||
|
||||
/// Cancels the download task if it is in progress.
|
||||
func cancel() {
|
||||
downloadTask?.cancel()
|
||||
downloadTask = nil
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
102
Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift
generated
Normal file
102
Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift
generated
Normal file
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// ImageContext.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2021/05/08.
|
||||
//
|
||||
// Copyright (c) 2021 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.
|
||||
|
||||
#if canImport(SwiftUI) && canImport(Combine)
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
extension KFImage {
|
||||
public class Context<HoldingView: KFImageHoldingView> {
|
||||
let source: Source?
|
||||
var options = KingfisherParsedOptionsInfo(
|
||||
KingfisherManager.shared.defaultOptions + [.loadDiskFileSynchronously]
|
||||
)
|
||||
|
||||
var configurations: [(HoldingView) -> HoldingView] = []
|
||||
var renderConfigurations: [(HoldingView.RenderingView) -> Void] = []
|
||||
var contentConfiguration: ((HoldingView) -> AnyView)? = nil
|
||||
|
||||
var cancelOnDisappear: Bool = false
|
||||
var placeholder: ((Progress) -> AnyView)? = nil
|
||||
|
||||
let onFailureDelegate = Delegate<KingfisherError, Void>()
|
||||
let onSuccessDelegate = Delegate<RetrieveImageResult, Void>()
|
||||
let onProgressDelegate = Delegate<(Int64, Int64), Void>()
|
||||
|
||||
var startLoadingBeforeViewAppear: Bool = false
|
||||
|
||||
init(source: Source?) {
|
||||
self.source = source
|
||||
}
|
||||
|
||||
func shouldApplyFade(cacheType: CacheType) -> Bool {
|
||||
options.forceTransition || cacheType == .none
|
||||
}
|
||||
|
||||
func fadeTransitionDuration(cacheType: CacheType) -> TimeInterval? {
|
||||
shouldApplyFade(cacheType: cacheType)
|
||||
? options.transition.fadeDuration
|
||||
: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageTransition {
|
||||
// Only for fade effect in SwiftUI.
|
||||
fileprivate var fadeDuration: TimeInterval? {
|
||||
switch self {
|
||||
case .fade(let duration):
|
||||
return duration
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
extension KFImage.Context: Hashable {
|
||||
public static func == (lhs: KFImage.Context<HoldingView>, rhs: KFImage.Context<HoldingView>) -> Bool {
|
||||
lhs.source == rhs.source &&
|
||||
lhs.options.processor.identifier == rhs.options.processor.identifier
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(source)
|
||||
hasher.combine(options.processor.identifier)
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(UIKit) && !os(watchOS)
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
extension KFAnimatedImage {
|
||||
public typealias Context = KFImage.Context
|
||||
typealias ImageBinder = KFImage.ImageBinder
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
96
Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift
generated
Normal file
96
Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift
generated
Normal file
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// KFAnimatedImage.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by wangxingbin on 2021/4/29.
|
||||
//
|
||||
// Copyright (c) 2021 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.
|
||||
|
||||
#if canImport(SwiftUI) && canImport(Combine) && canImport(UIKit) && !os(watchOS)
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
public struct KFAnimatedImage: KFImageProtocol {
|
||||
public typealias HoldingView = KFAnimatedImageViewRepresenter
|
||||
public var context: Context<HoldingView>
|
||||
public init(context: KFImage.Context<HoldingView>) {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
/// Configures current rendering view with a `block`. This block will be applied when the under-hood
|
||||
/// `AnimatedImageView` is created in `UIViewRepresentable.makeUIView(context:)`
|
||||
///
|
||||
/// - Parameter block: The block applies to the animated image view.
|
||||
/// - Returns: A `KFAnimatedImage` view that being configured by the `block`.
|
||||
public func configure(_ block: @escaping (HoldingView.RenderingView) -> Void) -> Self {
|
||||
context.renderConfigurations.append(block)
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapped `UIViewRepresentable` of `AnimatedImageView`
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
public struct KFAnimatedImageViewRepresenter: UIViewRepresentable, KFImageHoldingView {
|
||||
public typealias RenderingView = AnimatedImageView
|
||||
public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context<Self>) -> KFAnimatedImageViewRepresenter {
|
||||
KFAnimatedImageViewRepresenter(image: image, context: context)
|
||||
}
|
||||
|
||||
var image: KFCrossPlatformImage?
|
||||
let context: KFImage.Context<KFAnimatedImageViewRepresenter>
|
||||
|
||||
public func makeUIView(context: Context) -> AnimatedImageView {
|
||||
let view = AnimatedImageView()
|
||||
|
||||
self.context.renderConfigurations.forEach { $0(view) }
|
||||
|
||||
view.image = image
|
||||
|
||||
// Allow SwiftUI scale (fit/fill) working fine.
|
||||
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
view.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
||||
return view
|
||||
}
|
||||
|
||||
public func updateUIView(_ uiView: AnimatedImageView, context: Context) {
|
||||
uiView.image = image
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
struct KFAnimatedImage_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
KFAnimatedImage(source: .network(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF/1.gif")!))
|
||||
.onSuccess { r in
|
||||
print(r)
|
||||
}
|
||||
.placeholder {
|
||||
ProgressView()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
106
Pods/Kingfisher/Sources/SwiftUI/KFImage.swift
generated
Normal file
106
Pods/Kingfisher/Sources/SwiftUI/KFImage.swift
generated
Normal file
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// KFImage.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2019/06/26.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#if canImport(SwiftUI) && canImport(Combine)
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
public struct KFImage: KFImageProtocol {
|
||||
public var context: Context<Image>
|
||||
public init(context: Context<Image>) {
|
||||
self.context = context
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
extension Image: KFImageHoldingView {
|
||||
public typealias RenderingView = Image
|
||||
public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context<Self>) -> Image {
|
||||
Image(crossPlatformImage: image)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image compatibility.
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
extension KFImage {
|
||||
|
||||
public func resizable(
|
||||
capInsets: EdgeInsets = EdgeInsets(),
|
||||
resizingMode: Image.ResizingMode = .stretch) -> KFImage
|
||||
{
|
||||
configure { $0.resizable(capInsets: capInsets, resizingMode: resizingMode) }
|
||||
}
|
||||
|
||||
public func renderingMode(_ renderingMode: Image.TemplateRenderingMode?) -> KFImage {
|
||||
configure { $0.renderingMode(renderingMode) }
|
||||
}
|
||||
|
||||
public func interpolation(_ interpolation: Image.Interpolation) -> KFImage {
|
||||
configure { $0.interpolation(interpolation) }
|
||||
}
|
||||
|
||||
public func antialiased(_ isAntialiased: Bool) -> KFImage {
|
||||
configure { $0.antialiased(isAntialiased) }
|
||||
}
|
||||
|
||||
/// Starts the loading process of `self` immediately.
|
||||
///
|
||||
/// By default, a `KFImage` will not load its source until the `onAppear` is called. This is a lazily loading
|
||||
/// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a
|
||||
/// flickering since the loading does not happen immediately. Call this method if you want to start the load at once
|
||||
/// could help avoiding the flickering, with some performance trade-off.
|
||||
///
|
||||
/// - Deprecated: This is not necessary anymore since `@StateObject` is used for holding the image data.
|
||||
/// It does nothing now and please just remove it.
|
||||
///
|
||||
/// - Returns: The `Self` value with changes applied.
|
||||
@available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.")
|
||||
public func loadImmediately(_ start: Bool = true) -> KFImage {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
struct KFImage_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
KFImage.url(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png")!)
|
||||
.onSuccess { r in
|
||||
print(r)
|
||||
}
|
||||
.placeholder { p in
|
||||
ProgressView(p)
|
||||
}
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
158
Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift
generated
Normal file
158
Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift
generated
Normal file
@@ -0,0 +1,158 @@
|
||||
//
|
||||
// KFImageOptions.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2020/12/20.
|
||||
//
|
||||
// Copyright (c) 2020 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.
|
||||
|
||||
#if canImport(SwiftUI) && canImport(Combine)
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
// MARK: - KFImage creating.
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
extension KFImageProtocol {
|
||||
|
||||
/// Creates a `KFImage` for a given `Source`.
|
||||
/// - Parameters:
|
||||
/// - source: The `Source` object defines data information from network or a data provider.
|
||||
/// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`.
|
||||
public static func source(
|
||||
_ source: Source?
|
||||
) -> Self
|
||||
{
|
||||
Self.init(source: source)
|
||||
}
|
||||
|
||||
/// Creates a `KFImage` for a given `Resource`.
|
||||
/// - Parameters:
|
||||
/// - source: The `Resource` object defines data information like key or URL.
|
||||
/// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`.
|
||||
public static func resource(
|
||||
_ resource: Resource?
|
||||
) -> Self
|
||||
{
|
||||
source(resource?.convertToSource())
|
||||
}
|
||||
|
||||
/// Creates a `KFImage` for a given `URL`.
|
||||
/// - Parameters:
|
||||
/// - url: The URL where the image should be downloaded.
|
||||
/// - cacheKey: The key used to store the downloaded image in cache.
|
||||
/// If `nil`, the `absoluteString` of `url` is used as the cache key.
|
||||
/// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`.
|
||||
public static func url(
|
||||
_ url: URL?, cacheKey: String? = nil
|
||||
) -> Self
|
||||
{
|
||||
source(url?.convertToSource(overrideCacheKey: cacheKey))
|
||||
}
|
||||
|
||||
/// Creates a `KFImage` for a given `ImageDataProvider`.
|
||||
/// - Parameters:
|
||||
/// - provider: The `ImageDataProvider` object contains information about the data.
|
||||
/// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`.
|
||||
public static func dataProvider(
|
||||
_ provider: ImageDataProvider?
|
||||
) -> Self
|
||||
{
|
||||
source(provider?.convertToSource())
|
||||
}
|
||||
|
||||
/// Creates a builder for some given raw data and a cache key.
|
||||
/// - Parameters:
|
||||
/// - data: The data object from which the image should be created.
|
||||
/// - cacheKey: The key used to store the downloaded image in cache.
|
||||
/// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`.
|
||||
public static func data(
|
||||
_ data: Data?, cacheKey: String
|
||||
) -> Self
|
||||
{
|
||||
if let data = data {
|
||||
return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey))
|
||||
} else {
|
||||
return dataProvider(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
extension KFImageProtocol {
|
||||
/// Sets a placeholder `View` which shows when loading the image, with a progress parameter as input.
|
||||
/// - Parameter content: A view that describes the placeholder.
|
||||
/// - Returns: A `KFImage` view that contains `content` as its placeholder.
|
||||
public func placeholder<P: View>(@ViewBuilder _ content: @escaping (Progress) -> P) -> Self {
|
||||
context.placeholder = { progress in
|
||||
return AnyView(content(progress))
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets a placeholder `View` which shows when loading the image.
|
||||
/// - Parameter content: A view that describes the placeholder.
|
||||
/// - Returns: A `KFImage` view that contains `content` as its placeholder.
|
||||
public func placeholder<P: View>(@ViewBuilder _ content: @escaping () -> P) -> Self {
|
||||
placeholder { _ in content() }
|
||||
}
|
||||
|
||||
/// Sets cancelling the download task bound to `self` when the view disappearing.
|
||||
/// - Parameter flag: Whether cancel the task or not.
|
||||
/// - Returns: A `KFImage` view that cancels downloading task when disappears.
|
||||
public func cancelOnDisappear(_ flag: Bool) -> Self {
|
||||
context.cancelOnDisappear = flag
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets a fade transition for the image task.
|
||||
/// - Parameter duration: The duration of the fade transition.
|
||||
/// - Returns: A `KFImage` with changes applied.
|
||||
///
|
||||
/// Kingfisher will use the fade transition to animate the image in if it is downloaded from web.
|
||||
/// The transition will not happen when the
|
||||
/// image is retrieved from either memory or disk cache by default. If you need to do the transition even when
|
||||
/// the image being retrieved from cache, also call `forceRefresh()` on the returned `KFImage`.
|
||||
public func fade(duration: TimeInterval) -> Self {
|
||||
context.options.transition = .fade(duration)
|
||||
return self
|
||||
}
|
||||
|
||||
/// Sets whether to start the image loading before the view actually appears.
|
||||
///
|
||||
/// By default, Kingfisher performs a lazy loading for `KFImage`. The image loading won't start until the view's
|
||||
/// `onAppear` is called. However, sometimes you may want to trigger an aggressive loading for the view. By enabling
|
||||
/// this, the `KFImage` will try to load the view when its `body` is evaluated when the image loading is not yet
|
||||
/// started or a previous loading did fail.
|
||||
///
|
||||
/// - Parameter flag: Whether the image loading should happen before view appear. Default is `true`.
|
||||
/// - Returns: A `KFImage` with changes applied.
|
||||
///
|
||||
/// - Note: This is a temporary workaround for an issue from iOS 16, where the SwiftUI view's `onAppear` is not
|
||||
/// called when it is deeply embedded inside a `List` or `ForEach`.
|
||||
/// See [#1988](https://github.com/onevcat/Kingfisher/issues/1988). It may cause performance regression, especially
|
||||
/// if you have a lot of images to load in the view. Use it as your own risk.
|
||||
///
|
||||
public func startLoadingBeforeViewAppear(_ flag: Bool = true) -> Self {
|
||||
context.startLoadingBeforeViewAppear = flag
|
||||
return self
|
||||
}
|
||||
}
|
||||
#endif
|
||||
112
Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift
generated
Normal file
112
Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift
generated
Normal file
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// KFImageProtocol.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2021/05/08.
|
||||
//
|
||||
// Copyright (c) 2021 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.
|
||||
|
||||
#if canImport(SwiftUI) && canImport(Combine)
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
public protocol KFImageProtocol: View, KFOptionSetter {
|
||||
associatedtype HoldingView: KFImageHoldingView
|
||||
var context: KFImage.Context<HoldingView> { get set }
|
||||
init(context: KFImage.Context<HoldingView>)
|
||||
}
|
||||
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
extension KFImageProtocol {
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
KFImageRenderer<HoldingView>(
|
||||
context: context
|
||||
).id(context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a Kingfisher compatible image view to load image from the given `Source`.
|
||||
/// - Parameters:
|
||||
/// - source: The image `Source` defining where to load the target image.
|
||||
public init(source: Source?) {
|
||||
let context = KFImage.Context<HoldingView>(source: source)
|
||||
self.init(context: context)
|
||||
}
|
||||
|
||||
/// Creates a Kingfisher compatible image view to load image from the given `URL`.
|
||||
/// - Parameters:
|
||||
/// - source: The image `Source` defining where to load the target image.
|
||||
public init(_ url: URL?) {
|
||||
self.init(source: url?.convertToSource())
|
||||
}
|
||||
|
||||
/// Configures current image with a `block` and return another `Image` to use as the final content.
|
||||
///
|
||||
/// This block will be lazily applied when creating the final `Image`.
|
||||
///
|
||||
/// If multiple `configure` modifiers are added to the image, they will be evaluated by order. If you want to
|
||||
/// configure the input image (which is usually an `Image` value) to a non-`Image` value, use `contentConfigure`.
|
||||
///
|
||||
/// - Parameter block: The block applies to loaded image. The block should return an `Image` that is configured.
|
||||
/// - Returns: A `KFImage` view that configures internal `Image` with `block`.
|
||||
public func configure(_ block: @escaping (HoldingView) -> HoldingView) -> Self {
|
||||
context.configurations.append(block)
|
||||
return self
|
||||
}
|
||||
|
||||
/// Configures current image with a `block` and return a `View` to use as the final content.
|
||||
///
|
||||
/// This block will be lazily applied when creating the final `Image`.
|
||||
///
|
||||
/// If multiple `contentConfigure` modifiers are added to the image, only the last one will be stored and used.
|
||||
///
|
||||
/// - Parameter block: The block applies to the loaded image. The block should return a `View` that is configured.
|
||||
/// - Returns: A `KFImage` view that configures internal `Image` with `block`.
|
||||
public func contentConfigure<V: View>(_ block: @escaping (HoldingView) -> V) -> Self {
|
||||
context.contentConfiguration = { AnyView(block($0)) }
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
public protocol KFImageHoldingView: View {
|
||||
associatedtype RenderingView
|
||||
static func created(from image: KFCrossPlatformImage?, context: KFImage.Context<Self>) -> Self
|
||||
}
|
||||
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
extension KFImageProtocol {
|
||||
public var options: KingfisherParsedOptionsInfo {
|
||||
get { context.options }
|
||||
nonmutating set { context.options = newValue }
|
||||
}
|
||||
|
||||
public var onFailureDelegate: Delegate<KingfisherError, Void> { context.onFailureDelegate }
|
||||
public var onSuccessDelegate: Delegate<RetrieveImageResult, Void> { context.onSuccessDelegate }
|
||||
public var onProgressDelegate: Delegate<(Int64, Int64), Void> { context.onProgressDelegate }
|
||||
|
||||
public var delegateObserver: AnyObject { context }
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
129
Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift
generated
Normal file
129
Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift
generated
Normal file
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// KFImageRenderer.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2021/05/08.
|
||||
//
|
||||
// Copyright (c) 2021 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.
|
||||
|
||||
#if canImport(SwiftUI) && canImport(Combine)
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
/// A Kingfisher compatible SwiftUI `View` to load an image from a `Source`.
|
||||
/// Declaring a `KFImage` in a `View`'s body to trigger loading from the given `Source`.
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
struct KFImageRenderer<HoldingView> : View where HoldingView: KFImageHoldingView {
|
||||
|
||||
@StateObject var binder: KFImage.ImageBinder = .init()
|
||||
let context: KFImage.Context<HoldingView>
|
||||
|
||||
var body: some View {
|
||||
if context.startLoadingBeforeViewAppear && !binder.loadingOrSucceeded && !binder.animating {
|
||||
binder.markLoading()
|
||||
DispatchQueue.main.async { binder.start(context: context) }
|
||||
}
|
||||
|
||||
return ZStack {
|
||||
renderedImage().opacity(binder.loaded ? 1.0 : 0.0)
|
||||
if binder.loadedImage == nil {
|
||||
ZStack {
|
||||
if let placeholder = context.placeholder {
|
||||
placeholder(binder.progress)
|
||||
} else {
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
.onAppear { [weak binder = self.binder] in
|
||||
guard let binder = binder else {
|
||||
return
|
||||
}
|
||||
if !binder.loadingOrSucceeded {
|
||||
binder.start(context: context)
|
||||
}
|
||||
}
|
||||
.onDisappear { [weak binder = self.binder] in
|
||||
guard let binder = binder else {
|
||||
return
|
||||
}
|
||||
if context.cancelOnDisappear {
|
||||
binder.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Workaround for https://github.com/onevcat/Kingfisher/issues/1988
|
||||
// on iOS 16 there seems to be a bug that when in a List, the `onAppear` of the `ZStack` above in the
|
||||
// `binder.loadedImage == nil` not get called. Adding this empty `onAppear` fixes it and the life cycle can
|
||||
// work again.
|
||||
//
|
||||
// There is another "fix": adding an `else` clause and put a `Color.clear` there. But I believe this `onAppear`
|
||||
// should work better.
|
||||
//
|
||||
// It should be a bug in iOS 16, I guess it is some kinds of over-optimization in list cell loading caused it.
|
||||
.onAppear()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func renderedImage() -> some View {
|
||||
let configuredImage = context.configurations
|
||||
.reduce(HoldingView.created(from: binder.loadedImage, context: context)) {
|
||||
current, config in config(current)
|
||||
}
|
||||
if let contentConfiguration = context.contentConfiguration {
|
||||
contentConfiguration(configuredImage)
|
||||
} else {
|
||||
configuredImage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
extension Image {
|
||||
// Creates an Image with either UIImage or NSImage.
|
||||
init(crossPlatformImage: KFCrossPlatformImage?) {
|
||||
#if canImport(UIKit)
|
||||
self.init(uiImage: crossPlatformImage ?? KFCrossPlatformImage())
|
||||
#elseif canImport(AppKit)
|
||||
self.init(nsImage: crossPlatformImage ?? KFCrossPlatformImage())
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
|
||||
extension UIImage.Orientation {
|
||||
func toSwiftUI() -> Image.Orientation {
|
||||
switch self {
|
||||
case .down: return .down
|
||||
case .up: return .up
|
||||
case .left: return .left
|
||||
case .right: return .right
|
||||
case .upMirrored: return .upMirrored
|
||||
case .downMirrored: return .downMirrored
|
||||
case .leftMirrored: return .leftMirrored
|
||||
case .rightMirrored: return .rightMirrored
|
||||
@unknown default: return .up
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
34
Pods/Kingfisher/Sources/Utility/Box.swift
generated
Normal file
34
Pods/Kingfisher/Sources/Utility/Box.swift
generated
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// Box.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 2018/3/17.
|
||||
// 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
|
||||
|
||||
class Box<T> {
|
||||
var value: T
|
||||
|
||||
init(_ value: T) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
83
Pods/Kingfisher/Sources/Utility/CallbackQueue.swift
generated
Normal file
83
Pods/Kingfisher/Sources/Utility/CallbackQueue.swift
generated
Normal file
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// CallbackQueue.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2018/10/15.
|
||||
//
|
||||
// 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
|
||||
|
||||
public typealias ExecutionQueue = CallbackQueue
|
||||
|
||||
/// Represents callback queue behaviors when an calling of closure be dispatched.
|
||||
///
|
||||
/// - asyncMain: Dispatch the calling to `DispatchQueue.main` with an `async` behavior.
|
||||
/// - currentMainOrAsync: Dispatch the calling to `DispatchQueue.main` with an `async` behavior if current queue is not
|
||||
/// `.main`. Otherwise, call the closure immediately in current main queue.
|
||||
/// - untouch: Do not change the calling queue for closure.
|
||||
/// - dispatch: Dispatches to a specified `DispatchQueue`.
|
||||
public enum CallbackQueue {
|
||||
/// Dispatch the calling to `DispatchQueue.main` with an `async` behavior.
|
||||
case mainAsync
|
||||
/// Dispatch the calling to `DispatchQueue.main` with an `async` behavior if current queue is not
|
||||
/// `.main`. Otherwise, call the closure immediately in current main queue.
|
||||
case mainCurrentOrAsync
|
||||
/// Do not change the calling queue for closure.
|
||||
case untouch
|
||||
/// Dispatches to a specified `DispatchQueue`.
|
||||
case dispatch(DispatchQueue)
|
||||
|
||||
public func execute(_ block: @escaping () -> Void) {
|
||||
switch self {
|
||||
case .mainAsync:
|
||||
DispatchQueue.main.async { block() }
|
||||
case .mainCurrentOrAsync:
|
||||
DispatchQueue.main.safeAsync { block() }
|
||||
case .untouch:
|
||||
block()
|
||||
case .dispatch(let queue):
|
||||
queue.async { block() }
|
||||
}
|
||||
}
|
||||
|
||||
var queue: DispatchQueue {
|
||||
switch self {
|
||||
case .mainAsync: return .main
|
||||
case .mainCurrentOrAsync: return .main
|
||||
case .untouch: return OperationQueue.current?.underlyingQueue ?? .main
|
||||
case .dispatch(let queue): return queue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DispatchQueue {
|
||||
// This method will dispatch the `block` to self.
|
||||
// If `self` is the main queue, and current thread is main thread, the block
|
||||
// will be invoked immediately instead of being dispatched.
|
||||
func safeAsync(_ block: @escaping () -> Void) {
|
||||
if self === DispatchQueue.main && Thread.isMainThread {
|
||||
block()
|
||||
} else {
|
||||
async { block() }
|
||||
}
|
||||
}
|
||||
}
|
||||
132
Pods/Kingfisher/Sources/Utility/Delegate.swift
generated
Normal file
132
Pods/Kingfisher/Sources/Utility/Delegate.swift
generated
Normal file
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// Delegate.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2018/10/10.
|
||||
//
|
||||
// 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
|
||||
/// A class that keeps a weakly reference for `self` when implementing `onXXX` behaviors.
|
||||
/// Instead of remembering to keep `self` as weak in a stored closure:
|
||||
///
|
||||
/// ```swift
|
||||
/// // MyClass.swift
|
||||
/// var onDone: (() -> Void)?
|
||||
/// func done() {
|
||||
/// onDone?()
|
||||
/// }
|
||||
///
|
||||
/// // ViewController.swift
|
||||
/// var obj: MyClass?
|
||||
///
|
||||
/// func doSomething() {
|
||||
/// obj = MyClass()
|
||||
/// obj!.onDone = { [weak self] in
|
||||
/// self?.reportDone()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// You can create a `Delegate` and observe on `self`. Now, there is no retain cycle inside:
|
||||
///
|
||||
/// ```swift
|
||||
/// // MyClass.swift
|
||||
/// let onDone = Delegate<(), Void>()
|
||||
/// func done() {
|
||||
/// onDone.call()
|
||||
/// }
|
||||
///
|
||||
/// // ViewController.swift
|
||||
/// var obj: MyClass?
|
||||
///
|
||||
/// func doSomething() {
|
||||
/// obj = MyClass()
|
||||
/// obj!.onDone.delegate(on: self) { (self, _)
|
||||
/// // `self` here is shadowed and does not keep a strong ref.
|
||||
/// // So you can release both `MyClass` instance and `ViewController` instance.
|
||||
/// self.reportDone()
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
public class Delegate<Input, Output> {
|
||||
public init() {}
|
||||
|
||||
private var block: ((Input) -> Output?)?
|
||||
public func delegate<T: AnyObject>(on target: T, block: ((T, Input) -> Output)?) {
|
||||
self.block = { [weak target] input in
|
||||
guard let target = target else { return nil }
|
||||
return block?(target, input)
|
||||
}
|
||||
}
|
||||
|
||||
public func call(_ input: Input) -> Output? {
|
||||
return block?(input)
|
||||
}
|
||||
|
||||
public func callAsFunction(_ input: Input) -> Output? {
|
||||
return call(input)
|
||||
}
|
||||
}
|
||||
|
||||
extension Delegate where Input == Void {
|
||||
public func call() -> Output? {
|
||||
return call(())
|
||||
}
|
||||
|
||||
public func callAsFunction() -> Output? {
|
||||
return call()
|
||||
}
|
||||
}
|
||||
|
||||
extension Delegate where Input == Void, Output: OptionalProtocol {
|
||||
public func call() -> Output {
|
||||
return call(())
|
||||
}
|
||||
|
||||
public func callAsFunction() -> Output {
|
||||
return call()
|
||||
}
|
||||
}
|
||||
|
||||
extension Delegate where Output: OptionalProtocol {
|
||||
public func call(_ input: Input) -> Output {
|
||||
if let result = block?(input) {
|
||||
return result
|
||||
} else {
|
||||
return Output._createNil
|
||||
}
|
||||
}
|
||||
|
||||
public func callAsFunction(_ input: Input) -> Output {
|
||||
return call(input)
|
||||
}
|
||||
}
|
||||
|
||||
public protocol OptionalProtocol {
|
||||
static var _createNil: Self { get }
|
||||
}
|
||||
extension Optional : OptionalProtocol {
|
||||
public static var _createNil: Optional<Wrapped> {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
117
Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift
generated
Normal file
117
Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift
generated
Normal file
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// ExtensionHelpers.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2018/09/28.
|
||||
//
|
||||
// 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
|
||||
|
||||
extension CGFloat {
|
||||
var isEven: Bool {
|
||||
return truncatingRemainder(dividingBy: 2.0) == 0
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
|
||||
import AppKit
|
||||
extension NSBezierPath {
|
||||
convenience init(roundedRect rect: NSRect, topLeftRadius: CGFloat, topRightRadius: CGFloat,
|
||||
bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat)
|
||||
{
|
||||
self.init()
|
||||
|
||||
let maxCorner = min(rect.width, rect.height) / 2
|
||||
|
||||
let radiusTopLeft = min(maxCorner, max(0, topLeftRadius))
|
||||
let radiusTopRight = min(maxCorner, max(0, topRightRadius))
|
||||
let radiusBottomLeft = min(maxCorner, max(0, bottomLeftRadius))
|
||||
let radiusBottomRight = min(maxCorner, max(0, bottomRightRadius))
|
||||
|
||||
guard !rect.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
let topLeft = NSPoint(x: rect.minX, y: rect.maxY)
|
||||
let topRight = NSPoint(x: rect.maxX, y: rect.maxY)
|
||||
let bottomRight = NSPoint(x: rect.maxX, y: rect.minY)
|
||||
|
||||
move(to: NSPoint(x: rect.midX, y: rect.maxY))
|
||||
appendArc(from: topLeft, to: rect.origin, radius: radiusTopLeft)
|
||||
appendArc(from: rect.origin, to: bottomRight, radius: radiusBottomLeft)
|
||||
appendArc(from: bottomRight, to: topRight, radius: radiusBottomRight)
|
||||
appendArc(from: topRight, to: topLeft, radius: radiusTopRight)
|
||||
close()
|
||||
}
|
||||
|
||||
convenience init(roundedRect rect: NSRect, byRoundingCorners corners: RectCorner, radius: CGFloat) {
|
||||
let radiusTopLeft = corners.contains(.topLeft) ? radius : 0
|
||||
let radiusTopRight = corners.contains(.topRight) ? radius : 0
|
||||
let radiusBottomLeft = corners.contains(.bottomLeft) ? radius : 0
|
||||
let radiusBottomRight = corners.contains(.bottomRight) ? radius : 0
|
||||
|
||||
self.init(roundedRect: rect, topLeftRadius: radiusTopLeft, topRightRadius: radiusTopRight,
|
||||
bottomLeftRadius: radiusBottomLeft, bottomRightRadius: radiusBottomRight)
|
||||
}
|
||||
}
|
||||
|
||||
extension KFCrossPlatformImage {
|
||||
// macOS does not support scale. This is just for code compatibility across platforms.
|
||||
convenience init?(data: Data, scale: CGFloat) {
|
||||
self.init(data: data)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
extension RectCorner {
|
||||
var uiRectCorner: UIRectCorner {
|
||||
|
||||
var result: UIRectCorner = []
|
||||
|
||||
if contains(.topLeft) { result.insert(.topLeft) }
|
||||
if contains(.topRight) { result.insert(.topRight) }
|
||||
if contains(.bottomLeft) { result.insert(.bottomLeft) }
|
||||
if contains(.bottomRight) { result.insert(.bottomRight) }
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
extension Date {
|
||||
var isPast: Bool {
|
||||
return isPast(referenceDate: Date())
|
||||
}
|
||||
|
||||
func isPast(referenceDate: Date) -> Bool {
|
||||
return timeIntervalSince(referenceDate) <= 0
|
||||
}
|
||||
|
||||
// `Date` in memory is a wrap for `TimeInterval`. But in file attribute it can only accept `Int` number.
|
||||
// By default the system will `round` it. But it is not friendly for testing purpose.
|
||||
// So we always `ceil` the value when used for file attributes.
|
||||
var fileAttributeDate: Date {
|
||||
return Date(timeIntervalSince1970: ceil(timeIntervalSince1970))
|
||||
}
|
||||
}
|
||||
50
Pods/Kingfisher/Sources/Utility/Result.swift
generated
Normal file
50
Pods/Kingfisher/Sources/Utility/Result.swift
generated
Normal file
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// Result.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2018/09/22.
|
||||
//
|
||||
// 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
|
||||
|
||||
// These helper methods are not public since we do not want them to be exposed or cause any conflicting.
|
||||
// However, they are just wrapper of `ResultUtil` static methods.
|
||||
extension Result where Failure: Error {
|
||||
|
||||
/// Evaluates the given transform closures to create a single output value.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - onSuccess: A closure that transforms the success value.
|
||||
/// - onFailure: A closure that transforms the error value.
|
||||
/// - Returns: A single `Output` value.
|
||||
func match<Output>(
|
||||
onSuccess: (Success) -> Output,
|
||||
onFailure: (Failure) -> Output) -> Output
|
||||
{
|
||||
switch self {
|
||||
case let .success(value):
|
||||
return onSuccess(value)
|
||||
case let .failure(error):
|
||||
return onFailure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Pods/Kingfisher/Sources/Utility/Runtime.swift
generated
Normal file
35
Pods/Kingfisher/Sources/Utility/Runtime.swift
generated
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// Runtime.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 2018/10/12.
|
||||
//
|
||||
// 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
|
||||
|
||||
func getAssociatedObject<T>(_ object: Any, _ key: UnsafeRawPointer) -> T? {
|
||||
return objc_getAssociatedObject(object, key) as? T
|
||||
}
|
||||
|
||||
func setRetainedAssociatedObject<T>(_ object: Any, _ key: UnsafeRawPointer, _ value: T) {
|
||||
objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
110
Pods/Kingfisher/Sources/Utility/SizeExtensions.swift
generated
Normal file
110
Pods/Kingfisher/Sources/Utility/SizeExtensions.swift
generated
Normal file
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// SizeExtensions.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by onevcat on 2018/09/28.
|
||||
//
|
||||
// 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 CoreGraphics
|
||||
|
||||
extension CGSize: KingfisherCompatibleValue {}
|
||||
extension KingfisherWrapper where Base == CGSize {
|
||||
|
||||
/// Returns a size by resizing the `base` size to a target size under a given content mode.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - size: The target size to resize to.
|
||||
/// - contentMode: Content mode of the target size should be when resizing.
|
||||
/// - Returns: The resized size under the given `ContentMode`.
|
||||
public func resize(to size: CGSize, for contentMode: ContentMode) -> CGSize {
|
||||
switch contentMode {
|
||||
case .aspectFit:
|
||||
return constrained(size)
|
||||
case .aspectFill:
|
||||
return filling(size)
|
||||
case .none:
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a size by resizing the `base` size by making it aspect fitting the given `size`.
|
||||
///
|
||||
/// - Parameter size: The size in which the `base` should fit in.
|
||||
/// - Returns: The size fitted in by the input `size`, while keeps `base` aspect.
|
||||
public func constrained(_ size: CGSize) -> CGSize {
|
||||
let aspectWidth = round(aspectRatio * size.height)
|
||||
let aspectHeight = round(size.width / aspectRatio)
|
||||
|
||||
return aspectWidth > size.width ?
|
||||
CGSize(width: size.width, height: aspectHeight) :
|
||||
CGSize(width: aspectWidth, height: size.height)
|
||||
}
|
||||
|
||||
/// Returns a size by resizing the `base` size by making it aspect filling the given `size`.
|
||||
///
|
||||
/// - Parameter size: The size in which the `base` should fill.
|
||||
/// - Returns: The size be filled by the input `size`, while keeps `base` aspect.
|
||||
public func filling(_ size: CGSize) -> CGSize {
|
||||
let aspectWidth = round(aspectRatio * size.height)
|
||||
let aspectHeight = round(size.width / aspectRatio)
|
||||
|
||||
return aspectWidth < size.width ?
|
||||
CGSize(width: size.width, height: aspectHeight) :
|
||||
CGSize(width: aspectWidth, height: size.height)
|
||||
}
|
||||
|
||||
/// Returns a `CGRect` for which the `base` size is constrained to an input `size` at a given `anchor` point.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - size: The size in which the `base` should be constrained to.
|
||||
/// - anchor: An anchor point in which the size constraint should happen.
|
||||
/// - Returns: The result `CGRect` for the constraint operation.
|
||||
public func constrainedRect(for size: CGSize, anchor: CGPoint) -> CGRect {
|
||||
|
||||
let unifiedAnchor = CGPoint(x: anchor.x.clamped(to: 0.0...1.0),
|
||||
y: anchor.y.clamped(to: 0.0...1.0))
|
||||
|
||||
let x = unifiedAnchor.x * base.width - unifiedAnchor.x * size.width
|
||||
let y = unifiedAnchor.y * base.height - unifiedAnchor.y * size.height
|
||||
let r = CGRect(x: x, y: y, width: size.width, height: size.height)
|
||||
|
||||
let ori = CGRect(origin: .zero, size: base)
|
||||
return ori.intersection(r)
|
||||
}
|
||||
|
||||
private var aspectRatio: CGFloat {
|
||||
return base.height == 0.0 ? 1.0 : base.width / base.height
|
||||
}
|
||||
}
|
||||
|
||||
extension CGRect {
|
||||
func scaled(_ scale: CGFloat) -> CGRect {
|
||||
return CGRect(x: origin.x * scale, y: origin.y * scale,
|
||||
width: size.width * scale, height: size.height * scale)
|
||||
}
|
||||
}
|
||||
|
||||
extension Comparable {
|
||||
func clamped(to limits: ClosedRange<Self>) -> Self {
|
||||
return min(max(self, limits.lowerBound), limits.upperBound)
|
||||
}
|
||||
}
|
||||
278
Pods/Kingfisher/Sources/Utility/String+MD5.swift
generated
Normal file
278
Pods/Kingfisher/Sources/Utility/String+MD5.swift
generated
Normal file
@@ -0,0 +1,278 @@
|
||||
//
|
||||
// String+MD5.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by Wei Wang on 18/09/25.
|
||||
//
|
||||
// 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
|
||||
import CommonCrypto
|
||||
|
||||
extension String: KingfisherCompatibleValue { }
|
||||
extension KingfisherWrapper where Base == String {
|
||||
var md5: String {
|
||||
guard let data = base.data(using: .utf8) else {
|
||||
return base
|
||||
}
|
||||
|
||||
let message = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in
|
||||
return [UInt8](bytes)
|
||||
}
|
||||
|
||||
let MD5Calculator = MD5(message)
|
||||
let MD5Data = MD5Calculator.calculate()
|
||||
|
||||
var MD5String = String()
|
||||
for c in MD5Data {
|
||||
MD5String += String(format: "%02x", c)
|
||||
}
|
||||
return MD5String
|
||||
}
|
||||
|
||||
var ext: String? {
|
||||
var ext = ""
|
||||
if let index = base.lastIndex(of: ".") {
|
||||
let extRange = base.index(index, offsetBy: 1)..<base.endIndex
|
||||
ext = String(base[extRange])
|
||||
}
|
||||
guard let firstSeg = ext.split(separator: "@").first else {
|
||||
return nil
|
||||
}
|
||||
return firstSeg.count > 0 ? String(firstSeg) : nil
|
||||
}
|
||||
}
|
||||
|
||||
// array of bytes, little-endian representation
|
||||
func arrayOfBytes<T>(_ value: T, length: Int? = nil) -> [UInt8] {
|
||||
let totalBytes = length ?? (MemoryLayout<T>.size * 8)
|
||||
|
||||
let valuePointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
|
||||
valuePointer.pointee = value
|
||||
|
||||
let bytes = valuePointer.withMemoryRebound(to: UInt8.self, capacity: totalBytes) { (bytesPointer) -> [UInt8] in
|
||||
var bytes = [UInt8](repeating: 0, count: totalBytes)
|
||||
for j in 0..<min(MemoryLayout<T>.size, totalBytes) {
|
||||
bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
valuePointer.deinitialize(count: 1)
|
||||
valuePointer.deallocate()
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
extension Int {
|
||||
// Array of bytes with optional padding (little-endian)
|
||||
func bytes(_ totalBytes: Int = MemoryLayout<Int>.size) -> [UInt8] {
|
||||
return arrayOfBytes(self, length: totalBytes)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protocol HashProtocol {
|
||||
var message: [UInt8] { get }
|
||||
// Common part for hash calculation. Prepare header data.
|
||||
func prepare(_ len: Int) -> [UInt8]
|
||||
}
|
||||
|
||||
extension HashProtocol {
|
||||
|
||||
func prepare(_ len: Int) -> [UInt8] {
|
||||
var tmpMessage = message
|
||||
|
||||
// Step 1. Append Padding Bits
|
||||
tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message
|
||||
|
||||
// append "0" bit until message length in bits ≡ 448 (mod 512)
|
||||
var msgLength = tmpMessage.count
|
||||
var counter = 0
|
||||
|
||||
while msgLength % len != (len - 8) {
|
||||
counter += 1
|
||||
msgLength += 1
|
||||
}
|
||||
|
||||
tmpMessage += [UInt8](repeating: 0, count: counter)
|
||||
return tmpMessage
|
||||
}
|
||||
}
|
||||
|
||||
func toUInt32Array(_ slice: ArraySlice<UInt8>) -> [UInt32] {
|
||||
var result = [UInt32]()
|
||||
result.reserveCapacity(16)
|
||||
|
||||
for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout<UInt32>.size) {
|
||||
let d0 = UInt32(slice[idx.advanced(by: 3)]) << 24
|
||||
let d1 = UInt32(slice[idx.advanced(by: 2)]) << 16
|
||||
let d2 = UInt32(slice[idx.advanced(by: 1)]) << 8
|
||||
let d3 = UInt32(slice[idx])
|
||||
let val: UInt32 = d0 | d1 | d2 | d3
|
||||
|
||||
result.append(val)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
struct BytesIterator: IteratorProtocol {
|
||||
|
||||
let chunkSize: Int
|
||||
let data: [UInt8]
|
||||
|
||||
init(chunkSize: Int, data: [UInt8]) {
|
||||
self.chunkSize = chunkSize
|
||||
self.data = data
|
||||
}
|
||||
|
||||
var offset = 0
|
||||
|
||||
mutating func next() -> ArraySlice<UInt8>? {
|
||||
let end = min(chunkSize, data.count - offset)
|
||||
let result = data[offset..<offset + end]
|
||||
offset += result.count
|
||||
return result.count > 0 ? result : nil
|
||||
}
|
||||
}
|
||||
|
||||
struct BytesSequence: Sequence {
|
||||
let chunkSize: Int
|
||||
let data: [UInt8]
|
||||
|
||||
func makeIterator() -> BytesIterator {
|
||||
return BytesIterator(chunkSize: chunkSize, data: data)
|
||||
}
|
||||
}
|
||||
|
||||
func rotateLeft(_ value: UInt32, bits: UInt32) -> UInt32 {
|
||||
return ((value << bits) & 0xFFFFFFFF) | (value >> (32 - bits))
|
||||
}
|
||||
|
||||
class MD5: HashProtocol {
|
||||
|
||||
let message: [UInt8]
|
||||
|
||||
init (_ message: [UInt8]) {
|
||||
self.message = message
|
||||
}
|
||||
|
||||
// specifies the per-round shift amounts
|
||||
private let shifts: [UInt32] = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
|
||||
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
|
||||
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
|
||||
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21]
|
||||
|
||||
// binary integer part of the sines of integers (Radians)
|
||||
private let sines: [UInt32] = [0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
|
||||
0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
|
||||
0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
|
||||
0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
|
||||
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
|
||||
0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
|
||||
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
|
||||
0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
|
||||
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
|
||||
0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
|
||||
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05,
|
||||
0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
|
||||
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
|
||||
0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
|
||||
0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
|
||||
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391]
|
||||
|
||||
private let hashes: [UInt32] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476]
|
||||
|
||||
func calculate() -> [UInt8] {
|
||||
var tmpMessage = prepare(64)
|
||||
tmpMessage.reserveCapacity(tmpMessage.count + 4)
|
||||
|
||||
// hash values
|
||||
var hh = hashes
|
||||
|
||||
// Step 2. Append Length a 64-bit representation of lengthInBits
|
||||
let lengthInBits = (message.count * 8)
|
||||
let lengthBytes = lengthInBits.bytes(64 / 8)
|
||||
tmpMessage += lengthBytes.reversed()
|
||||
|
||||
// Process the message in successive 512-bit chunks:
|
||||
let chunkSizeBytes = 512 / 8 // 64
|
||||
|
||||
for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) {
|
||||
// break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15
|
||||
let M = toUInt32Array(chunk)
|
||||
assert(M.count == 16, "Invalid array")
|
||||
|
||||
// Initialize hash value for this chunk:
|
||||
var A: UInt32 = hh[0]
|
||||
var B: UInt32 = hh[1]
|
||||
var C: UInt32 = hh[2]
|
||||
var D: UInt32 = hh[3]
|
||||
|
||||
var dTemp: UInt32 = 0
|
||||
|
||||
// Main loop
|
||||
for j in 0 ..< sines.count {
|
||||
var g = 0
|
||||
var F: UInt32 = 0
|
||||
|
||||
switch j {
|
||||
case 0...15:
|
||||
F = (B & C) | ((~B) & D)
|
||||
g = j
|
||||
case 16...31:
|
||||
F = (D & B) | (~D & C)
|
||||
g = (5 * j + 1) % 16
|
||||
case 32...47:
|
||||
F = B ^ C ^ D
|
||||
g = (3 * j + 5) % 16
|
||||
case 48...63:
|
||||
F = C ^ (B | (~D))
|
||||
g = (7 * j) % 16
|
||||
default:
|
||||
break
|
||||
}
|
||||
dTemp = D
|
||||
D = C
|
||||
C = B
|
||||
B = B &+ rotateLeft((A &+ F &+ sines[j] &+ M[g]), bits: shifts[j])
|
||||
A = dTemp
|
||||
}
|
||||
|
||||
hh[0] = hh[0] &+ A
|
||||
hh[1] = hh[1] &+ B
|
||||
hh[2] = hh[2] &+ C
|
||||
hh[3] = hh[3] &+ D
|
||||
}
|
||||
var result = [UInt8]()
|
||||
result.reserveCapacity(hh.count / 4)
|
||||
|
||||
hh.forEach {
|
||||
let itemLE = $0.littleEndian
|
||||
let r1 = UInt8(itemLE & 0xff)
|
||||
let r2 = UInt8((itemLE >> 8) & 0xff)
|
||||
let r3 = UInt8((itemLE >> 16) & 0xff)
|
||||
let r4 = UInt8((itemLE >> 24) & 0xff)
|
||||
result += [r1, r2, r3, r4]
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
725
Pods/Kingfisher/Sources/Views/AnimatedImageView.swift
generated
Normal file
725
Pods/Kingfisher/Sources/Views/AnimatedImageView.swift
generated
Normal file
@@ -0,0 +1,725 @@
|
||||
//
|
||||
// AnimatableImageView.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by bl4ckra1sond3tre on 4/22/16.
|
||||
//
|
||||
// The AnimatableImageView, AnimatedFrame and Animator is a modified version of
|
||||
// some classes from kaishin's Gifu project (https://github.com/kaishin/Gifu)
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2019 Reda Lemeden.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// The name and characters used in the demo of this software are property of their
|
||||
// respective owners.
|
||||
|
||||
#if !os(watchOS)
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
import ImageIO
|
||||
|
||||
/// Protocol of `AnimatedImageView`.
|
||||
public protocol AnimatedImageViewDelegate: AnyObject {
|
||||
|
||||
/// Called after the animatedImageView has finished each animation loop.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - imageView: The `AnimatedImageView` that is being animated.
|
||||
/// - count: The looped count.
|
||||
func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt)
|
||||
|
||||
/// Called after the `AnimatedImageView` has reached the max repeat count.
|
||||
///
|
||||
/// - Parameter imageView: The `AnimatedImageView` that is being animated.
|
||||
func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView)
|
||||
}
|
||||
|
||||
extension AnimatedImageViewDelegate {
|
||||
public func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) {}
|
||||
public func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) {}
|
||||
}
|
||||
|
||||
let KFRunLoopModeCommon = RunLoop.Mode.common
|
||||
|
||||
/// Represents a subclass of `UIImageView` for displaying animated image.
|
||||
/// Different from showing animated image in a normal `UIImageView` (which load all frames at one time),
|
||||
/// `AnimatedImageView` only tries to load several frames (defined by `framePreloadCount`) to reduce memory usage.
|
||||
/// It provides a tradeoff between memory usage and CPU time. If you have a memory issue when using a normal image
|
||||
/// view to load GIF data, you could give this class a try.
|
||||
///
|
||||
/// Kingfisher supports setting GIF animated data to either `UIImageView` and `AnimatedImageView` out of box. So
|
||||
/// it would be fairly easy to switch between them.
|
||||
open class AnimatedImageView: UIImageView {
|
||||
/// Proxy object for preventing a reference cycle between the `CADDisplayLink` and `AnimatedImageView`.
|
||||
class TargetProxy {
|
||||
private weak var target: AnimatedImageView?
|
||||
|
||||
init(target: AnimatedImageView) {
|
||||
self.target = target
|
||||
}
|
||||
|
||||
@objc func onScreenUpdate() {
|
||||
target?.updateFrameIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumeration that specifies repeat count of GIF
|
||||
public enum RepeatCount: Equatable {
|
||||
case once
|
||||
case finite(count: UInt)
|
||||
case infinite
|
||||
|
||||
public static func ==(lhs: RepeatCount, rhs: RepeatCount) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.finite(l), .finite(r)):
|
||||
return l == r
|
||||
case (.once, .once),
|
||||
(.infinite, .infinite):
|
||||
return true
|
||||
case (.once, .finite(let count)),
|
||||
(.finite(let count), .once):
|
||||
return count == 1
|
||||
case (.once, _),
|
||||
(.infinite, _),
|
||||
(.finite, _):
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public property
|
||||
/// Whether automatically play the animation when the view become visible. Default is `true`.
|
||||
public var autoPlayAnimatedImage = true
|
||||
|
||||
/// The count of the frames should be preloaded before shown.
|
||||
public var framePreloadCount = 10
|
||||
|
||||
/// Specifies whether the GIF frames should be pre-scaled to the image view's size or not.
|
||||
/// If the downloaded image is larger than the image view's size, it will help to reduce some memory use.
|
||||
/// Default is `true`.
|
||||
public var needsPrescaling = true
|
||||
|
||||
/// Decode the GIF frames in background thread before using. It will decode frames data and do a off-screen
|
||||
/// rendering to extract pixel information in background. This can reduce the main thread CPU usage.
|
||||
public var backgroundDecode = true
|
||||
|
||||
/// The animation timer's run loop mode. Default is `RunLoop.Mode.common`.
|
||||
/// Set this property to `RunLoop.Mode.default` will make the animation pause during UIScrollView scrolling.
|
||||
public var runLoopMode = KFRunLoopModeCommon {
|
||||
willSet {
|
||||
guard runLoopMode != newValue else { return }
|
||||
stopAnimating()
|
||||
displayLink.remove(from: .main, forMode: runLoopMode)
|
||||
displayLink.add(to: .main, forMode: newValue)
|
||||
startAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
/// The repeat count. The animated image will keep animate until it the loop count reaches this value.
|
||||
/// Setting this value to another one will reset current animation.
|
||||
///
|
||||
/// Default is `.infinite`, which means the animation will last forever.
|
||||
public var repeatCount = RepeatCount.infinite {
|
||||
didSet {
|
||||
if oldValue != repeatCount {
|
||||
reset()
|
||||
setNeedsDisplay()
|
||||
layer.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delegate of this `AnimatedImageView` object. See `AnimatedImageViewDelegate` protocol for more.
|
||||
public weak var delegate: AnimatedImageViewDelegate?
|
||||
|
||||
/// The `Animator` instance that holds the frames of a specific image in memory.
|
||||
public private(set) var animator: Animator?
|
||||
|
||||
// MARK: - Private property
|
||||
// Dispatch queue used for preloading images.
|
||||
private lazy var preloadQueue: DispatchQueue = {
|
||||
return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue")
|
||||
}()
|
||||
|
||||
// A flag to avoid invalidating the displayLink on deinit if it was never created, because displayLink is so lazy.
|
||||
private var isDisplayLinkInitialized: Bool = false
|
||||
|
||||
// A display link that keeps calling the `updateFrame` method on every screen refresh.
|
||||
private lazy var displayLink: CADisplayLink = {
|
||||
isDisplayLinkInitialized = true
|
||||
let displayLink = CADisplayLink(target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate))
|
||||
displayLink.add(to: .main, forMode: runLoopMode)
|
||||
displayLink.isPaused = true
|
||||
return displayLink
|
||||
}()
|
||||
|
||||
// MARK: - Override
|
||||
override open var image: KFCrossPlatformImage? {
|
||||
didSet {
|
||||
if image != oldValue {
|
||||
reset()
|
||||
}
|
||||
setNeedsDisplay()
|
||||
layer.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
open override var isHighlighted: Bool {
|
||||
get {
|
||||
super.isHighlighted
|
||||
}
|
||||
set {
|
||||
// Highlighted image is unsupported for animated images.
|
||||
// See https://github.com/onevcat/Kingfisher/issues/1679
|
||||
if displayLink.isPaused {
|
||||
super.isHighlighted = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for Apple xcframework creating issue on Apple TV in Swift 5.8.
|
||||
// https://github.com/apple/swift/issues/66015
|
||||
#if os(tvOS)
|
||||
public override init(image: UIImage?, highlightedImage: UIImage?) {
|
||||
super.init(image: image, highlightedImage: highlightedImage)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
#endif
|
||||
|
||||
deinit {
|
||||
if isDisplayLinkInitialized {
|
||||
displayLink.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
override open var isAnimating: Bool {
|
||||
if isDisplayLinkInitialized {
|
||||
return !displayLink.isPaused
|
||||
} else {
|
||||
return super.isAnimating
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts the animation.
|
||||
override open func startAnimating() {
|
||||
guard !isAnimating else { return }
|
||||
guard let animator = animator else { return }
|
||||
guard !animator.isReachMaxRepeatCount else { return }
|
||||
|
||||
displayLink.isPaused = false
|
||||
}
|
||||
|
||||
/// Stops the animation.
|
||||
override open func stopAnimating() {
|
||||
super.stopAnimating()
|
||||
if isDisplayLinkInitialized {
|
||||
displayLink.isPaused = true
|
||||
}
|
||||
}
|
||||
|
||||
override open func display(_ layer: CALayer) {
|
||||
layer.contents = animator?.currentFrameImage?.cgImage ?? image?.cgImage
|
||||
}
|
||||
|
||||
override open func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
didMove()
|
||||
}
|
||||
|
||||
override open func didMoveToSuperview() {
|
||||
super.didMoveToSuperview()
|
||||
didMove()
|
||||
}
|
||||
|
||||
// This is for back compatibility that using regular `UIImageView` to show animated image.
|
||||
override func shouldPreloadAllAnimation() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Reset the animator.
|
||||
private func reset() {
|
||||
animator = nil
|
||||
if let image = image, let frameSource = image.kf.frameSource {
|
||||
#if os(xrOS)
|
||||
let targetSize = bounds.scaled(UITraitCollection.current.displayScale).size
|
||||
#else
|
||||
let targetSize = bounds.scaled(UIScreen.main.scale).size
|
||||
#endif
|
||||
let animator = Animator(
|
||||
frameSource: frameSource,
|
||||
contentMode: contentMode,
|
||||
size: targetSize,
|
||||
imageSize: image.kf.size,
|
||||
imageScale: image.kf.scale,
|
||||
framePreloadCount: framePreloadCount,
|
||||
repeatCount: repeatCount,
|
||||
preloadQueue: preloadQueue)
|
||||
animator.delegate = self
|
||||
animator.needsPrescaling = needsPrescaling
|
||||
animator.backgroundDecode = backgroundDecode
|
||||
animator.prepareFramesAsynchronously()
|
||||
self.animator = animator
|
||||
}
|
||||
didMove()
|
||||
}
|
||||
|
||||
private func didMove() {
|
||||
if autoPlayAnimatedImage && animator != nil {
|
||||
if let _ = superview, let _ = window {
|
||||
startAnimating()
|
||||
} else {
|
||||
stopAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the current frame with the displayLink duration.
|
||||
private func updateFrameIfNeeded() {
|
||||
guard let animator = animator else {
|
||||
return
|
||||
}
|
||||
|
||||
guard !animator.isFinished else {
|
||||
stopAnimating()
|
||||
delegate?.animatedImageViewDidFinishAnimating(self)
|
||||
return
|
||||
}
|
||||
|
||||
let duration: CFTimeInterval
|
||||
|
||||
// CA based display link is opt-out from ProMotion by default.
|
||||
// So the duration and its FPS might not match.
|
||||
// See [#718](https://github.com/onevcat/Kingfisher/issues/718)
|
||||
// By setting CADisableMinimumFrameDuration to YES in Info.plist may
|
||||
// cause the preferredFramesPerSecond being 0
|
||||
let preferredFramesPerSecond = displayLink.preferredFramesPerSecond
|
||||
if preferredFramesPerSecond == 0 {
|
||||
duration = displayLink.duration
|
||||
} else {
|
||||
// Some devices (like iPad Pro 10.5) will have a different FPS.
|
||||
duration = 1.0 / TimeInterval(preferredFramesPerSecond)
|
||||
}
|
||||
|
||||
animator.shouldChangeFrame(with: duration) { [weak self] hasNewFrame in
|
||||
if hasNewFrame {
|
||||
self?.layer.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol AnimatorDelegate: AnyObject {
|
||||
func animator(_ animator: AnimatedImageView.Animator, didPlayAnimationLoops count: UInt)
|
||||
}
|
||||
|
||||
extension AnimatedImageView: AnimatorDelegate {
|
||||
func animator(_ animator: Animator, didPlayAnimationLoops count: UInt) {
|
||||
delegate?.animatedImageView(self, didPlayAnimationLoops: count)
|
||||
}
|
||||
}
|
||||
|
||||
extension AnimatedImageView {
|
||||
|
||||
// Represents a single frame in a GIF.
|
||||
struct AnimatedFrame {
|
||||
|
||||
// The image to display for this frame. Its value is nil when the frame is removed from the buffer.
|
||||
let image: UIImage?
|
||||
|
||||
// The duration that this frame should remain active.
|
||||
let duration: TimeInterval
|
||||
|
||||
// A placeholder frame with no image assigned.
|
||||
// Used to replace frames that are no longer needed in the animation.
|
||||
var placeholderFrame: AnimatedFrame {
|
||||
return AnimatedFrame(image: nil, duration: duration)
|
||||
}
|
||||
|
||||
// Whether this frame instance contains an image or not.
|
||||
var isPlaceholder: Bool {
|
||||
return image == nil
|
||||
}
|
||||
|
||||
// Returns a new instance from an optional image.
|
||||
//
|
||||
// - parameter image: An optional `UIImage` instance to be assigned to the new frame.
|
||||
// - returns: An `AnimatedFrame` instance.
|
||||
func makeAnimatedFrame(image: UIImage?) -> AnimatedFrame {
|
||||
return AnimatedFrame(image: image, duration: duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AnimatedImageView {
|
||||
|
||||
// MARK: - Animator
|
||||
|
||||
/// An animator which used to drive the data behind `AnimatedImageView`.
|
||||
public class Animator {
|
||||
private let size: CGSize
|
||||
|
||||
private let imageSize: CGSize
|
||||
private let imageScale: CGFloat
|
||||
|
||||
/// The maximum count of image frames that needs preload.
|
||||
public let maxFrameCount: Int
|
||||
|
||||
private let frameSource: ImageFrameSource
|
||||
private let maxRepeatCount: RepeatCount
|
||||
|
||||
private let maxTimeStep: TimeInterval = 1.0
|
||||
private let animatedFrames = SafeArray<AnimatedFrame>()
|
||||
private var frameCount = 0
|
||||
private var timeSinceLastFrameChange: TimeInterval = 0.0
|
||||
private var currentRepeatCount: UInt = 0
|
||||
|
||||
var isFinished: Bool = false
|
||||
|
||||
var needsPrescaling = true
|
||||
|
||||
var backgroundDecode = true
|
||||
|
||||
weak var delegate: AnimatorDelegate?
|
||||
|
||||
// Total duration of one animation loop
|
||||
var loopDuration: TimeInterval = 0
|
||||
|
||||
/// The image of the current frame.
|
||||
public var currentFrameImage: UIImage? {
|
||||
return frame(at: currentFrameIndex)
|
||||
}
|
||||
|
||||
/// The duration of the current active frame duration.
|
||||
public var currentFrameDuration: TimeInterval {
|
||||
return duration(at: currentFrameIndex)
|
||||
}
|
||||
|
||||
/// The index of the current animation frame.
|
||||
public internal(set) var currentFrameIndex = 0 {
|
||||
didSet {
|
||||
previousFrameIndex = oldValue
|
||||
}
|
||||
}
|
||||
|
||||
var previousFrameIndex = 0 {
|
||||
didSet {
|
||||
preloadQueue.async {
|
||||
self.updatePreloadedFrames()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isReachMaxRepeatCount: Bool {
|
||||
switch maxRepeatCount {
|
||||
case .once:
|
||||
return currentRepeatCount >= 1
|
||||
case .finite(let maxCount):
|
||||
return currentRepeatCount >= maxCount
|
||||
case .infinite:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the current frame is the last frame or not in the animation sequence.
|
||||
public var isLastFrame: Bool {
|
||||
return currentFrameIndex == frameCount - 1
|
||||
}
|
||||
|
||||
var preloadingIsNeeded: Bool {
|
||||
return maxFrameCount < frameCount - 1
|
||||
}
|
||||
|
||||
var contentMode = UIView.ContentMode.scaleToFill
|
||||
|
||||
private lazy var preloadQueue: DispatchQueue = {
|
||||
return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue")
|
||||
}()
|
||||
|
||||
/// Creates an animator with image source reference.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - source: The reference of animated image.
|
||||
/// - mode: Content mode of the `AnimatedImageView`.
|
||||
/// - size: Size of the `AnimatedImageView`.
|
||||
/// - imageSize: Size of the `KingfisherWrapper`.
|
||||
/// - imageScale: Scale of the `KingfisherWrapper`.
|
||||
/// - count: Count of frames needed to be preloaded.
|
||||
/// - repeatCount: The repeat count should this animator uses.
|
||||
/// - preloadQueue: Dispatch queue used for preloading images.
|
||||
convenience init(imageSource source: CGImageSource,
|
||||
contentMode mode: UIView.ContentMode,
|
||||
size: CGSize,
|
||||
imageSize: CGSize,
|
||||
imageScale: CGFloat,
|
||||
framePreloadCount count: Int,
|
||||
repeatCount: RepeatCount,
|
||||
preloadQueue: DispatchQueue) {
|
||||
let frameSource = CGImageFrameSource(data: nil, imageSource: source, options: nil)
|
||||
self.init(frameSource: frameSource,
|
||||
contentMode: mode,
|
||||
size: size,
|
||||
imageSize: imageSize,
|
||||
imageScale: imageScale,
|
||||
framePreloadCount: count,
|
||||
repeatCount: repeatCount,
|
||||
preloadQueue: preloadQueue)
|
||||
}
|
||||
|
||||
/// Creates an animator with a custom image frame source.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - frameSource: The reference of animated image.
|
||||
/// - mode: Content mode of the `AnimatedImageView`.
|
||||
/// - size: Size of the `AnimatedImageView`.
|
||||
/// - imageSize: Size of the `KingfisherWrapper`.
|
||||
/// - imageScale: Scale of the `KingfisherWrapper`.
|
||||
/// - count: Count of frames needed to be preloaded.
|
||||
/// - repeatCount: The repeat count should this animator uses.
|
||||
/// - preloadQueue: Dispatch queue used for preloading images.
|
||||
init(frameSource source: ImageFrameSource,
|
||||
contentMode mode: UIView.ContentMode,
|
||||
size: CGSize,
|
||||
imageSize: CGSize,
|
||||
imageScale: CGFloat,
|
||||
framePreloadCount count: Int,
|
||||
repeatCount: RepeatCount,
|
||||
preloadQueue: DispatchQueue) {
|
||||
self.frameSource = source
|
||||
self.contentMode = mode
|
||||
self.size = size
|
||||
self.imageSize = imageSize
|
||||
self.imageScale = imageScale
|
||||
self.maxFrameCount = count
|
||||
self.maxRepeatCount = repeatCount
|
||||
self.preloadQueue = preloadQueue
|
||||
|
||||
GraphicsContext.begin(size: imageSize, scale: imageScale)
|
||||
}
|
||||
|
||||
deinit {
|
||||
resetAnimatedFrames()
|
||||
GraphicsContext.end()
|
||||
}
|
||||
|
||||
/// Gets the image frame of a given index.
|
||||
/// - Parameter index: The index of desired image.
|
||||
/// - Returns: The decoded image at the frame. `nil` if the index is out of bound or the image is not yet loaded.
|
||||
public func frame(at index: Int) -> KFCrossPlatformImage? {
|
||||
return animatedFrames[index]?.image
|
||||
}
|
||||
|
||||
public func duration(at index: Int) -> TimeInterval {
|
||||
return animatedFrames[index]?.duration ?? .infinity
|
||||
}
|
||||
|
||||
func prepareFramesAsynchronously() {
|
||||
frameCount = frameSource.frameCount
|
||||
animatedFrames.reserveCapacity(frameCount)
|
||||
preloadQueue.async { [weak self] in
|
||||
self?.setupAnimatedFrames()
|
||||
}
|
||||
}
|
||||
|
||||
func shouldChangeFrame(with duration: CFTimeInterval, handler: (Bool) -> Void) {
|
||||
incrementTimeSinceLastFrameChange(with: duration)
|
||||
|
||||
if currentFrameDuration > timeSinceLastFrameChange {
|
||||
handler(false)
|
||||
} else {
|
||||
resetTimeSinceLastFrameChange()
|
||||
incrementCurrentFrameIndex()
|
||||
handler(true)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupAnimatedFrames() {
|
||||
resetAnimatedFrames()
|
||||
|
||||
var duration: TimeInterval = 0
|
||||
|
||||
(0..<frameCount).forEach { index in
|
||||
let frameDuration = frameSource.duration(at: index)
|
||||
duration += min(frameDuration, maxTimeStep)
|
||||
animatedFrames.append(AnimatedFrame(image: nil, duration: frameDuration))
|
||||
|
||||
if index > maxFrameCount { return }
|
||||
animatedFrames[index] = animatedFrames[index]?.makeAnimatedFrame(image: loadFrame(at: index))
|
||||
}
|
||||
|
||||
self.loopDuration = duration
|
||||
}
|
||||
|
||||
private func resetAnimatedFrames() {
|
||||
animatedFrames.removeAll()
|
||||
}
|
||||
|
||||
private func loadFrame(at index: Int) -> UIImage? {
|
||||
let resize = needsPrescaling && size != .zero
|
||||
let maxSize = resize ? size : nil
|
||||
guard let cgImage = frameSource.frame(at: index, maxSize: maxSize) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if #available(iOS 15, tvOS 15, *) {
|
||||
// From iOS 15, a plain image loading causes iOS calling `-[_UIImageCGImageContent initWithCGImage:scale:]`
|
||||
// in ImageIO, which holds the image ref on the creating thread.
|
||||
// To get a workaround, create another image ref and use that to create the final image. This leads to
|
||||
// some performance loss, but there is little we can do.
|
||||
// https://github.com/onevcat/Kingfisher/issues/1844
|
||||
guard let context = GraphicsContext.current(size: imageSize, scale: imageScale, inverting: true, cgImage: cgImage),
|
||||
let decodedImageRef = cgImage.decoded(on: context, scale: imageScale)
|
||||
else {
|
||||
return KFCrossPlatformImage(cgImage: cgImage)
|
||||
}
|
||||
|
||||
return KFCrossPlatformImage(cgImage: decodedImageRef)
|
||||
} else {
|
||||
let image = KFCrossPlatformImage(cgImage: cgImage)
|
||||
if backgroundDecode {
|
||||
guard let context = GraphicsContext.current(size: imageSize, scale: imageScale, inverting: true, cgImage: cgImage) else {
|
||||
return image
|
||||
}
|
||||
return image.kf.decoded(on: context)
|
||||
} else {
|
||||
return image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePreloadedFrames() {
|
||||
guard preloadingIsNeeded else {
|
||||
return
|
||||
}
|
||||
|
||||
let previousFrame = animatedFrames[previousFrameIndex]
|
||||
animatedFrames[previousFrameIndex] = previousFrame?.placeholderFrame
|
||||
// ensure the image dealloc in main thread
|
||||
defer {
|
||||
if let image = previousFrame?.image {
|
||||
DispatchQueue.main.async {
|
||||
_ = image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preloadIndexes(start: currentFrameIndex).forEach { index in
|
||||
guard let currentAnimatedFrame = animatedFrames[index] else { return }
|
||||
if !currentAnimatedFrame.isPlaceholder { return }
|
||||
animatedFrames[index] = currentAnimatedFrame.makeAnimatedFrame(image: loadFrame(at: index))
|
||||
}
|
||||
}
|
||||
|
||||
private func incrementCurrentFrameIndex() {
|
||||
let wasLastFrame = isLastFrame
|
||||
currentFrameIndex = increment(frameIndex: currentFrameIndex)
|
||||
if isLastFrame {
|
||||
currentRepeatCount += 1
|
||||
if isReachMaxRepeatCount {
|
||||
isFinished = true
|
||||
|
||||
// Notify the delegate here because the animation is stopping.
|
||||
delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount)
|
||||
}
|
||||
} else if wasLastFrame {
|
||||
|
||||
// Notify the delegate that the loop completed
|
||||
delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount)
|
||||
}
|
||||
}
|
||||
|
||||
private func incrementTimeSinceLastFrameChange(with duration: TimeInterval) {
|
||||
timeSinceLastFrameChange += min(maxTimeStep, duration)
|
||||
}
|
||||
|
||||
private func resetTimeSinceLastFrameChange() {
|
||||
timeSinceLastFrameChange -= currentFrameDuration
|
||||
}
|
||||
|
||||
private func increment(frameIndex: Int, by value: Int = 1) -> Int {
|
||||
return (frameIndex + value) % frameCount
|
||||
}
|
||||
|
||||
private func preloadIndexes(start index: Int) -> [Int] {
|
||||
let nextIndex = increment(frameIndex: index)
|
||||
let lastIndex = increment(frameIndex: index, by: maxFrameCount)
|
||||
|
||||
if lastIndex >= nextIndex {
|
||||
return [Int](nextIndex...lastIndex)
|
||||
} else {
|
||||
return [Int](nextIndex..<frameCount) + [Int](0...lastIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SafeArray<Element> {
|
||||
private var array: Array<Element> = []
|
||||
private let lock = NSLock()
|
||||
|
||||
subscript(index: Int) -> Element? {
|
||||
get {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return array.indices ~= index ? array[index] : nil
|
||||
}
|
||||
|
||||
set {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
if let newValue = newValue, array.indices ~= index {
|
||||
array[index] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var count : Int {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return array.count
|
||||
}
|
||||
|
||||
func reserveCapacity(_ count: Int) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
array.reserveCapacity(count)
|
||||
}
|
||||
|
||||
func append(_ element: Element) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
array += [element]
|
||||
}
|
||||
|
||||
func removeAll() {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
array = []
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
233
Pods/Kingfisher/Sources/Views/Indicator.swift
generated
Normal file
233
Pods/Kingfisher/Sources/Views/Indicator.swift
generated
Normal file
@@ -0,0 +1,233 @@
|
||||
//
|
||||
// Indicator.swift
|
||||
// Kingfisher
|
||||
//
|
||||
// Created by João D. Moreira on 30/08/16.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#if !os(watchOS)
|
||||
|
||||
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
|
||||
import AppKit
|
||||
public typealias IndicatorView = NSView
|
||||
#else
|
||||
import UIKit
|
||||
public typealias IndicatorView = UIView
|
||||
#endif
|
||||
|
||||
/// Represents the activity indicator type which should be added to
|
||||
/// an image view when an image is being downloaded.
|
||||
///
|
||||
/// - none: No indicator.
|
||||
/// - activity: Uses the system activity indicator.
|
||||
/// - image: Uses an image as indicator. GIF is supported.
|
||||
/// - custom: Uses a custom indicator. The type of associated value should conform to the `Indicator` protocol.
|
||||
public enum IndicatorType {
|
||||
/// No indicator.
|
||||
case none
|
||||
/// Uses the system activity indicator.
|
||||
case activity
|
||||
/// Uses an image as indicator. GIF is supported.
|
||||
case image(imageData: Data)
|
||||
/// Uses a custom indicator. The type of associated value should conform to the `Indicator` protocol.
|
||||
case custom(indicator: Indicator)
|
||||
}
|
||||
|
||||
/// An indicator type which can be used to show the download task is in progress.
|
||||
public protocol Indicator {
|
||||
|
||||
/// Called when the indicator should start animating.
|
||||
func startAnimatingView()
|
||||
|
||||
/// Called when the indicator should stop animating.
|
||||
func stopAnimatingView()
|
||||
|
||||
/// Center offset of the indicator. Kingfisher will use this value to determine the position of
|
||||
/// indicator in the super view.
|
||||
var centerOffset: CGPoint { get }
|
||||
|
||||
/// The indicator view which would be added to the super view.
|
||||
var view: IndicatorView { get }
|
||||
|
||||
/// The size strategy used when adding the indicator to image view.
|
||||
/// - Parameter imageView: The super view of indicator.
|
||||
func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy
|
||||
}
|
||||
|
||||
public enum IndicatorSizeStrategy {
|
||||
case intrinsicSize
|
||||
case full
|
||||
case size(CGSize)
|
||||
}
|
||||
|
||||
extension Indicator {
|
||||
|
||||
/// Default implementation of `centerOffset` of `Indicator`. The default value is `.zero`, means that there is
|
||||
/// no offset for the indicator view.
|
||||
public var centerOffset: CGPoint { return .zero }
|
||||
|
||||
/// Default implementation of `centerOffset` of `Indicator`. The default value is `.full`, means that the indicator
|
||||
/// will pin to the same height and width as the image view.
|
||||
public func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy {
|
||||
return .full
|
||||
}
|
||||
}
|
||||
|
||||
// Displays a NSProgressIndicator / UIActivityIndicatorView
|
||||
final class ActivityIndicator: Indicator {
|
||||
|
||||
#if os(macOS)
|
||||
private let activityIndicatorView: NSProgressIndicator
|
||||
#else
|
||||
private let activityIndicatorView: UIActivityIndicatorView
|
||||
#endif
|
||||
private var animatingCount = 0
|
||||
|
||||
var view: IndicatorView {
|
||||
return activityIndicatorView
|
||||
}
|
||||
|
||||
func startAnimatingView() {
|
||||
if animatingCount == 0 {
|
||||
#if os(macOS)
|
||||
activityIndicatorView.startAnimation(nil)
|
||||
#else
|
||||
activityIndicatorView.startAnimating()
|
||||
#endif
|
||||
activityIndicatorView.isHidden = false
|
||||
}
|
||||
animatingCount += 1
|
||||
}
|
||||
|
||||
func stopAnimatingView() {
|
||||
animatingCount = max(animatingCount - 1, 0)
|
||||
if animatingCount == 0 {
|
||||
#if os(macOS)
|
||||
activityIndicatorView.stopAnimation(nil)
|
||||
#else
|
||||
activityIndicatorView.stopAnimating()
|
||||
#endif
|
||||
activityIndicatorView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy {
|
||||
return .intrinsicSize
|
||||
}
|
||||
|
||||
init() {
|
||||
#if os(macOS)
|
||||
activityIndicatorView = NSProgressIndicator(frame: CGRect(x: 0, y: 0, width: 16, height: 16))
|
||||
activityIndicatorView.controlSize = .small
|
||||
activityIndicatorView.style = .spinning
|
||||
#else
|
||||
let indicatorStyle: UIActivityIndicatorView.Style
|
||||
|
||||
#if os(tvOS)
|
||||
if #available(tvOS 13.0, *) {
|
||||
indicatorStyle = UIActivityIndicatorView.Style.large
|
||||
} else {
|
||||
indicatorStyle = UIActivityIndicatorView.Style.white
|
||||
}
|
||||
#elseif os(xrOS)
|
||||
indicatorStyle = UIActivityIndicatorView.Style.medium
|
||||
#else
|
||||
if #available(iOS 13.0, * ) {
|
||||
indicatorStyle = UIActivityIndicatorView.Style.medium
|
||||
} else {
|
||||
indicatorStyle = UIActivityIndicatorView.Style.gray
|
||||
}
|
||||
#endif
|
||||
|
||||
activityIndicatorView = UIActivityIndicatorView(style: indicatorStyle)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(UIKit)
|
||||
extension UIActivityIndicatorView.Style {
|
||||
#if compiler(>=5.1)
|
||||
#else
|
||||
static let large = UIActivityIndicatorView.Style.white
|
||||
#if !os(tvOS)
|
||||
static let medium = UIActivityIndicatorView.Style.gray
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - ImageIndicator
|
||||
// Displays an ImageView. Supports gif
|
||||
final class ImageIndicator: Indicator {
|
||||
private let animatedImageIndicatorView: KFCrossPlatformImageView
|
||||
|
||||
var view: IndicatorView {
|
||||
return animatedImageIndicatorView
|
||||
}
|
||||
|
||||
init?(
|
||||
imageData data: Data,
|
||||
processor: ImageProcessor = DefaultImageProcessor.default,
|
||||
options: KingfisherParsedOptionsInfo? = nil)
|
||||
{
|
||||
var options = options ?? KingfisherParsedOptionsInfo(nil)
|
||||
// Use normal image view to show animations, so we need to preload all animation data.
|
||||
if !options.preloadAllAnimationData {
|
||||
options.preloadAllAnimationData = true
|
||||
}
|
||||
|
||||
guard let image = processor.process(item: .data(data), options: options) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
animatedImageIndicatorView = KFCrossPlatformImageView()
|
||||
animatedImageIndicatorView.image = image
|
||||
|
||||
#if os(macOS)
|
||||
// Need for gif to animate on macOS
|
||||
animatedImageIndicatorView.imageScaling = .scaleNone
|
||||
animatedImageIndicatorView.canDrawSubviewsIntoLayer = true
|
||||
#else
|
||||
animatedImageIndicatorView.contentMode = .center
|
||||
#endif
|
||||
}
|
||||
|
||||
func startAnimatingView() {
|
||||
#if os(macOS)
|
||||
animatedImageIndicatorView.animates = true
|
||||
#else
|
||||
animatedImageIndicatorView.startAnimating()
|
||||
#endif
|
||||
animatedImageIndicatorView.isHidden = false
|
||||
}
|
||||
|
||||
func stopAnimatingView() {
|
||||
#if os(macOS)
|
||||
animatedImageIndicatorView.animates = false
|
||||
#else
|
||||
animatedImageIndicatorView.stopAnimating()
|
||||
#endif
|
||||
animatedImageIndicatorView.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user