Files
SideStore/Pods/Nuke/Sources/Core/ImageRequest.swift
2023-03-01 16:38:49 -06:00

472 lines
17 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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))
}
}