Files
SideStore/Pods/Nuke/Sources/Core/ImageRequest.swift

472 lines
17 KiB
Swift
Raw Normal View History

// The MIT License (MIT)
//
// Copyright (c) 2015-2022 Alexander Grebenyuk (github.com/kean).
import Foundation
import Combine
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#endif
#if os(watchOS)
import WatchKit
#endif
#if os(macOS)
import Cocoa
#endif
// MARK: - ImageRequest
/// Represents an image request.
public struct ImageRequest: CustomStringConvertible {
// MARK: Parameters
/// Returns the request `URLRequest`.
///
/// Returns `nil` for publisher-based requests.
public var urlRequest: URLRequest? {
switch ref.resource {
case .url(let url): return url.map { URLRequest(url: $0) } // create lazily
case .urlRequest(let urlRequest): return urlRequest
case .publisher: return nil
}
}
/// Returns the request `URL`.
///
/// Returns `nil` for publisher-based requests.
public var url: URL? {
switch ref.resource {
case .url(let url): return url
case .urlRequest(let request): return request.url
case .publisher: return nil
}
}
/// Returns the ID of the underlying image. For URL-based request, it's an
/// image URL. For publisher a custom ID.
public var imageId: String? {
switch ref.resource {
case .url(let url): return url?.absoluteString
case .urlRequest(let urlRequest): return urlRequest.url?.absoluteString
case .publisher(let publisher): return publisher.id
}
}
/// The relative priority of the request. The priority affects the order in
/// which the requests are performed. `.normal` by default.
public var priority: Priority {
get { ref.priority }
set { mutate { $0.priority = newValue } }
}
/// Processor to be applied to the image. Empty by default.
public var processors: [ImageProcessing] {
get { ref.processors }
set { mutate { $0.processors = newValue } }
}
/// The request options.
public var options: Options {
get { ref.options }
set { mutate { $0.options = newValue } }
}
/// Custom info passed alongside the request.
public var userInfo: [UserInfoKey: Any] {
get { ref.userInfo ?? [:] }
set { mutate { $0.userInfo = newValue } }
}
/// The priority affecting the order in which the requests are performed.
public enum Priority: Int, Comparable {
case veryLow = 0, low, normal, high, veryHigh
public static func < (lhs: Priority, rhs: Priority) -> Bool {
lhs.rawValue < rhs.rawValue
}
}
/// A key use in `userInfo`.
public struct UserInfoKey: Hashable, ExpressibleByStringLiteral {
public let rawValue: String
public init(_ rawValue: String) {
self.rawValue = rawValue
}
public init(stringLiteral value: String) {
self.rawValue = value
}
/// By default, a pipeline uses URLs as unique image identifiers for
/// caching and task coalescing. You can override this behavior by
/// providing an `imageIdKey` instead. For example, you can use it to remove
/// transient query parameters from the request.
///
/// ```
/// let request = ImageRequest(
/// url: URL(string: "http://example.com/image.jpeg?token=123"),
/// userInfo: [.imageIdKey: "http://example.com/image.jpeg"]
/// )
/// ```
public static let imageIdKey: ImageRequest.UserInfoKey = "github.com/kean/nuke/imageId"
/// The image scale to be used. By default, the scale matches the scale
/// of the current display.
public static let scaleKey: ImageRequest.UserInfoKey = "github.com/kean/nuke/scale"
/// Specifies whether the pipeline should retreive or generate a thumbnail
/// instead of a full image. The thumbnail creation is generally significantly
/// more efficient, especially in terms of memory usage, than image resizing
/// (`ImageProcessors.Resize`).
///
/// - note: You must be using the default image decoder to make it work.
public static let thumbnailKey: ImageRequest.UserInfoKey = "github.com/kean/nuke/thumbmnailKey"
}
// MARK: Initializers
/// Initializes a request with the given URL.
///
/// - parameter url: The request URL.
/// - parameter processors: Processors to be apply to the image. `[]` by default.
/// - parameter priority: The priority of the request, `.normal` by default.
/// - parameter options: Image loading options. `[]` by default.
/// - parameter userInfo: Custom info passed alongside the request. `nil` by default.
///
/// ```swift
/// let request = ImageRequest(
/// url: URL(string: "http://..."),
/// processors: [ImageProcessors.Resize(size: imageView.bounds.size)],
/// priority: .high
/// )
/// ```
public init(url: URL?,
processors: [ImageProcessing] = [],
priority: Priority = .normal,
options: Options = [],
userInfo: [UserInfoKey: Any]? = nil) {
self.ref = Container(
resource: Resource.url(url),
processors: processors,
priority: priority,
options: options,
userInfo: userInfo
)
}
/// Initializes a request with the given request.
///
/// - parameter urlRequest: The URLRequest describing the image request.
/// - parameter processors: Processors to be apply to the image. `[]` by default.
/// - parameter priority: The priority of the request, `.normal` by default.
/// - parameter options: Image loading options. `[]` by default.
/// - parameter userInfo: Custom info passed alongside the request. `nil` by default.
///
/// ```swift
/// let request = ImageRequest(
/// url: URLRequest(url: URL(string: "http://...")),
/// processors: [ImageProcessors.Resize(size: imageView.bounds.size)],
/// priority: .high
/// )
/// ```
public init(urlRequest: URLRequest,
processors: [ImageProcessing] = [],
priority: Priority = .normal,
options: Options = [],
userInfo: [UserInfoKey: Any]? = nil) {
self.ref = Container(
resource: Resource.urlRequest(urlRequest),
processors: processors,
priority: priority,
options: options,
userInfo: userInfo
)
}
/// Initializes a request with the given data publisher.
///
/// - parameter id: Uniquely identifies the image data.
/// - parameter data: A data publisher to be used for fetching image data.
/// - parameter processors: Processors to be apply to the image. `[]` by default.
/// - parameter priority: The priority of the request, `.normal` by default.
/// - parameter options: Image loading options. `[]` by default.
/// - parameter userInfo: Custom info passed alongside the request. `nil` by default.
///
/// For example, here is how you can use it with Photos framework (the
/// `imageDataPublisher()` API is a convenience extension).
///
/// ```swift
/// let request = ImageRequest(
/// id: asset.localIdentifier,
/// data: PHAssetManager.imageDataPublisher(for: asset)
/// )
/// ```
///
/// - warning: If you don't want data to be stored in the disk cache, make
/// sure to create a pipeline without it or disable it on a per-request basis.
/// You can also disable it dynamically using `ImagePipelineDelegate`.
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
public init<P>(id: String, data: P,
processors: [ImageProcessing] = [],
priority: Priority = .normal,
options: Options = [],
userInfo: [UserInfoKey: Any]? = nil) where P: Publisher, P.Output == Data {
// It could technically be implemented without any special change to the
// pipeline by using a custom DataLoader, disabling resumable data, and
// passing a publisher in the request userInfo.
self.ref = Container(
resource: .publisher(DataPublisher(id: id, data)),
processors: processors,
priority: priority,
options: options,
userInfo: userInfo
)
}
// MARK: Options
/// Image request options.
public struct Options: OptionSet, Hashable {
/// Returns a raw value.
public let rawValue: UInt16
/// Initialializes options with a given raw values.
public init(rawValue: UInt16) {
self.rawValue = rawValue
}
/// Disables memory cache reads (`ImageCaching`).
public static let disableMemoryCacheReads = Options(rawValue: 1 << 0)
/// Disables memory cache writes (`ImageCaching`).
public static let disableMemoryCacheWrites = Options(rawValue: 1 << 1)
/// Disables both memory cache reads and writes (`ImageCaching`).
public static let disableMemoryCache: Options = [.disableMemoryCacheReads, .disableMemoryCacheWrites]
/// Disables disk cache reads (`DataCaching`).
public static let disableDiskCacheReads = Options(rawValue: 1 << 2)
/// Disables disk cache writes (`DataCaching`).
public static let disableDiskCacheWrites = Options(rawValue: 1 << 3)
/// Disables both disk cache reads and writes (`DataCaching`).
public static let disableDiskCache: Options = [.disableDiskCacheReads, .disableDiskCacheWrites]
/// The image should be loaded only from the originating source.
///
/// This option only works `ImageCaching` and `DataCaching`, but not
/// `URLCache`. If you want to ignore `URLCache`, initialize the request
/// with `URLRequest` with the respective policy
public static let reloadIgnoringCachedData: Options = [.disableMemoryCacheReads, .disableDiskCacheReads]
/// Use existing cache data and fail if no cached data is available.
public static let returnCacheDataDontLoad = Options(rawValue: 1 << 4)
}
/// Thumbnail options.
///
/// For more info, see https://developer.apple.com/documentation/imageio/cgimagesource/image_source_option_dictionary_keys
public struct ThumbnailOptions: Hashable {
/// The maximum width and height in pixels of a thumbnail. If this key
/// is not specified, the width and height of a thumbnail is not limited
/// and thumbnails may be as big as the image itself.
public var maxPixelSize: CGFloat
/// Whether a thumbnail should be automatically created for an image if
/// a thumbnail isn't present in the image source file. The thumbnail is
/// created from the full image, subject to the limit specified by
/// `maxPixelSize`.
///
/// By default, `true`.
public var createThumbnailFromImageIfAbsent = true
/// Whether a thumbnail should be created from the full image even if a
/// thumbnail is present in the image source file. The thumbnail is created
/// from the full image, subject to the limit specified by
/// `maxPixelSize`.
///
/// By default, `true`.
public var createThumbnailFromImageAlways = true
/// Whether the thumbnail should be rotated and scaled according to the
/// orientation and pixel aspect ratio of the full image.
///
/// By default, `true`.
public var createThumbnailWithTransform = true
/// Specifies whether image decoding and caching should happen at image
/// creation time.
///
/// By default, `true`.
public var shouldCacheImmediately = true
public init(maxPixelSize: CGFloat,
createThumbnailFromImageIfAbsent: Bool = true,
createThumbnailFromImageAlways: Bool = true,
createThumbnailWithTransform: Bool = true,
shouldCacheImmediately: Bool = true) {
self.maxPixelSize = maxPixelSize
self.createThumbnailFromImageIfAbsent = createThumbnailFromImageIfAbsent
self.createThumbnailFromImageAlways = createThumbnailFromImageAlways
self.createThumbnailWithTransform = createThumbnailWithTransform
self.shouldCacheImmediately = shouldCacheImmediately
}
var identifier: String {
"com.github/kean/nuke/thumbnail?mxs=\(maxPixelSize),options=\(createThumbnailFromImageIfAbsent)\(createThumbnailFromImageAlways)\(createThumbnailWithTransform)\(shouldCacheImmediately)"
}
}
// MARK: Internal
private(set) var ref: Container
private mutating func mutate(_ closure: (Container) -> Void) {
if !isKnownUniquelyReferenced(&ref) {
ref = Container(ref)
}
closure(ref)
}
/// Just like many Swift built-in types, `ImageRequest` uses CoW approach to
/// avoid memberwise retain/releases when `ImageRequest` is passed around.
final class Container {
// It's benefitial to put resource before priority and options because
// of the resource size/stride of 9/16. Priority (1 byte) and Options
// (2 bytes) slot just right in the remaining space.
let resource: Resource
fileprivate(set) var priority: Priority
fileprivate(set) var options: Options
fileprivate(set) var processors: [ImageProcessing]
fileprivate(set) var userInfo: [UserInfoKey: Any]?
// After trimming down the request size, it is no longer
// as beneficial using CoW for ImageRequest, but there
// still is a small but measurable difference.
deinit {
#if TRACK_ALLOCATIONS
Allocations.decrement("ImageRequest.Container")
#endif
}
/// Creates a resource with a default processor.
init(resource: Resource, processors: [ImageProcessing], priority: Priority, options: Options, userInfo: [UserInfoKey: Any]?) {
self.resource = resource
self.processors = processors
self.priority = priority
self.options = options
self.userInfo = userInfo
#if TRACK_ALLOCATIONS
Allocations.increment("ImageRequest.Container")
#endif
}
/// Creates a copy.
init(_ ref: Container) {
self.resource = ref.resource
self.processors = ref.processors
self.priority = ref.priority
self.options = ref.options
self.userInfo = ref.userInfo
#if TRACK_ALLOCATIONS
Allocations.increment("ImageRequest.Container")
#endif
}
}
// Every case takes 8 bytes and the enum 9 bytes overall (use stride!)
enum Resource: CustomStringConvertible {
case url(URL?)
case urlRequest(URLRequest)
case publisher(DataPublisher)
var description: String {
switch self {
case .url(let url): return "\(url?.absoluteString ?? "nil")"
case .urlRequest(let urlRequest): return "\(urlRequest)"
case .publisher(let data): return "\(data)"
}
}
}
public var description: String {
"ImageRequest(resource: \(ref.resource), priority: \(priority), processors: \(processors), options: \(options), userInfo: \(userInfo))"
}
func withProcessors(_ processors: [ImageProcessing]) -> ImageRequest {
var request = self
request.processors = processors
return request
}
var preferredImageId: String {
if let imageId = ref.userInfo?[.imageIdKey] as? String {
return imageId
}
return imageId ?? ""
}
var thubmnail: ThumbnailOptions? {
ref.userInfo?[.thumbnailKey] as? ThumbnailOptions
}
var scale: CGFloat? {
guard let scale = ref.userInfo?[.scaleKey] as? NSNumber else {
return nil
}
return CGFloat(scale.floatValue)
}
var publisher: DataPublisher? {
guard case .publisher(let publisher) = ref.resource else {
return nil
}
return publisher
}
}
// MARK: - ImageRequestConvertible
/// Represents a type that can be converted to an `ImageRequest`.
public protocol ImageRequestConvertible {
func asImageRequest() -> ImageRequest
}
extension ImageRequest: ImageRequestConvertible {
public func asImageRequest() -> ImageRequest {
self
}
}
extension URL: ImageRequestConvertible {
public func asImageRequest() -> ImageRequest {
ImageRequest(url: self)
}
}
extension Optional: ImageRequestConvertible where Wrapped == URL {
public func asImageRequest() -> ImageRequest {
ImageRequest(url: self)
}
}
extension URLRequest: ImageRequestConvertible {
public func asImageRequest() -> ImageRequest {
ImageRequest(urlRequest: self)
}
}
extension String: ImageRequestConvertible {
public func asImageRequest() -> ImageRequest {
ImageRequest(url: URL(string: self))
}
}