mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-10 15:23:27 +01:00
472 lines
17 KiB
Swift
472 lines
17 KiB
Swift
// 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))
|
||
}
|
||
}
|