This commit is contained in:
DDIsFriend
2023-08-24 16:46:39 +08:00
parent 887a468768
commit 1a0943017a
139 changed files with 24162 additions and 13640 deletions

View 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)
}
}

View 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)
}
}
}

View 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)
}
}

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

View 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
}
}
}

View 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 }
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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))
}
}

View 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
}
}
}

View 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
View 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
}
}

View 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
}
}

View 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

View 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
}
}
}

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

View 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
View 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

View 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)
}
}

View 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
View 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)
}
}

View 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
}

View 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
}
}

View 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)
}
}

View 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()
}
}
}

View 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

View 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

View 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)
}
}

View 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))
}
}
}

View 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
}
}

View 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)
}
}

View 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

View 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
}
}
}

View 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
}
}

View 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
}
}

View 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))
}
}
}
}

View 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)
}
}

View 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)
}
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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
}
}

View 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() }
}
}
}

View 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
}
}

View 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))
}
}

View 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)
}
}
}

View 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)
}

View 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)
}
}

View 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
}
}

View 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

View 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