diff --git a/Nuke/AsyncImageTask.swift b/Nuke/AsyncImageTask.swift deleted file mode 100644 index 4178413..0000000 --- a/Nuke/AsyncImageTask.swift +++ /dev/null @@ -1,76 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -#if canImport(UIKit) -import UIKit -#endif - -#if canImport(AppKit) -import AppKit -#endif - -/// A task performed by the ``ImagePipeline``. Use ``ImagePipeline/imageTask(with:)-7s0fc`` -/// to create a task. -public final class AsyncImageTask: Sendable { - private let imageTask: ImageTask - private let task: Task - - /// Updates the priority of the task, even if it is already running. - public var priority: ImageRequest.Priority { - get { imageTask.priority } - set { imageTask.priority = newValue } - } - - /// The fetched image. - public var image: PlatformImage { - get async throws { - try await response.image - } - } - - /// The image response. - public var response: ImageResponse { - get async throws { - try await withTaskCancellationHandler { - try await task.value - } onCancel: { - self.cancel() - } - } - } - - /// Returns all images responses including the previews for progressive images. - public let previews: AsyncStream - - /// Returns the current download progress. Returns zeros before the download - /// is started and the expected size of the resource is known. - public let progress: AsyncStream - - init(imageTask: ImageTask, - task: Task, - progress: AsyncStream, - previews: AsyncStream) { - self.imageTask = imageTask - self.task = task - self.progress = progress - self.previews = previews - } - - /// Marks task as being cancelled. - /// - /// The pipeline will immediately cancel any work associated with a task - /// unless there is an equivalent outstanding task running. - public func cancel() { - imageTask.cancel() - } -} - -// Making it Sendable because the closures are set once right after initialization -// and are never mutated afterward. -final class AsyncTaskContext: @unchecked Sendable { - var progress: AsyncStream.Continuation? - var previews: AsyncStream.Continuation? -} diff --git a/Nuke/Caching/Cache.swift b/Nuke/Caching/Cache.swift deleted file mode 100644 index b35a189..0000000 --- a/Nuke/Caching/Cache.swift +++ /dev/null @@ -1,211 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -#if os(iOS) || os(tvOS) || os(visionOS) -import UIKit.UIApplication -#endif - -// Internal memory-cache implementation. -final class Cache: @unchecked Sendable { - // Can't use `NSCache` because it is not LRU - - struct Configuration { - var costLimit: Int - var countLimit: Int - var ttl: TimeInterval? - var entryCostLimit: Double - } - - var conf: Configuration { - get { withLock { _conf } } - set { withLock { _conf = newValue } } - } - - private var _conf: Configuration { - didSet { _trim() } - } - - var totalCost: Int { - withLock { _totalCost } - } - - var totalCount: Int { - withLock { map.count } - } - - private var _totalCost = 0 - private var map = [Key: LinkedList.Node]() - private let list = LinkedList() - private let lock: os_unfair_lock_t - private let memoryPressure: DispatchSourceMemoryPressure - private var notificationObserver: AnyObject? - - init(costLimit: Int, countLimit: Int) { - self._conf = Configuration(costLimit: costLimit, countLimit: countLimit, ttl: nil, entryCostLimit: 0.1) - - self.lock = .allocate(capacity: 1) - self.lock.initialize(to: os_unfair_lock()) - - self.memoryPressure = DispatchSource.makeMemoryPressureSource(eventMask: [.warning, .critical], queue: .main) - self.memoryPressure.setEventHandler { [weak self] in - self?.removeAllCachedValues() - } - self.memoryPressure.resume() - -#if os(iOS) || os(tvOS) || os(visionOS) - registerForEnterBackground() -#endif - } - - deinit { - lock.deinitialize(count: 1) - lock.deallocate() - - memoryPressure.cancel() - } - -#if os(iOS) || os(tvOS) || os(visionOS) - private func registerForEnterBackground() { - notificationObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in - self?.clearCacheOnEnterBackground() - } - } -#endif - - func value(forKey key: Key) -> Value? { - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - - guard let node = map[key] else { - return nil - } - - guard !node.value.isExpired else { - _remove(node: node) - return nil - } - - // bubble node up to make it last added (most recently used) - list.remove(node) - list.append(node) - - return node.value.value - } - - func set(_ value: Value, forKey key: Key, cost: Int = 0, ttl: TimeInterval? = nil) { - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - - // Take care of overflow or cache size big enough to fit any - // reasonable content (and also of costLimit = Int.max). - let sanitizedEntryLimit = max(0, min(_conf.entryCostLimit, 1)) - guard _conf.costLimit > 2_147_483_647 || cost < Int(sanitizedEntryLimit * Double(_conf.costLimit)) else { - return - } - - let ttl = ttl ?? _conf.ttl - let expiration = ttl.map { Date() + $0 } - let entry = Entry(value: value, key: key, cost: cost, expiration: expiration) - _add(entry) - _trim() // _trim is extremely fast, it's OK to call it each time - } - - @discardableResult - func removeValue(forKey key: Key) -> Value? { - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - - guard let node = map[key] else { - return nil - } - _remove(node: node) - return node.value.value - } - - private func _add(_ element: Entry) { - if let existingNode = map[element.key] { - // This is slightly faster than calling _remove because of the - // skipped dictionary access - list.remove(existingNode) - _totalCost -= existingNode.value.cost - } - map[element.key] = list.append(element) - _totalCost += element.cost - } - - private func _remove(node: LinkedList.Node) { - list.remove(node) - map[node.value.key] = nil - _totalCost -= node.value.cost - } - - func removeAllCachedValues() { - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - - map.removeAll() - list.removeAllElements() - _totalCost = 0 - } - - private dynamic func clearCacheOnEnterBackground() { - // Remove most of the stored items when entering background. - // This behavior is similar to `NSCache` (which removes all - // items). This feature is not documented and may be subject - // to change in future Nuke versions. - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - - _trim(toCost: Int(Double(_conf.costLimit) * 0.1)) - _trim(toCount: Int(Double(_conf.countLimit) * 0.1)) - } - - private func _trim() { - _trim(toCost: _conf.costLimit) - _trim(toCount: _conf.countLimit) - } - - func trim(toCost limit: Int) { - withLock { _trim(toCost: limit) } - } - - private func _trim(toCost limit: Int) { - _trim(while: { _totalCost > limit }) - } - - func trim(toCount limit: Int) { - withLock { _trim(toCount: limit) } - } - - private func _trim(toCount limit: Int) { - _trim(while: { map.count > limit }) - } - - private func _trim(while condition: () -> Bool) { - while condition(), let node = list.first { // least recently used - _remove(node: node) - } - } - - private func withLock(_ closure: () -> T) -> T { - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - return closure() - } - - private struct Entry { - let value: Value - let key: Key - let cost: Int - let expiration: Date? - var isExpired: Bool { - guard let expiration else { - return false - } - return expiration.timeIntervalSinceNow < 0 - } - } -} diff --git a/Nuke/Caching/DataCache.swift b/Nuke/Caching/DataCache.swift deleted file mode 100644 index 8a63cf1..0000000 --- a/Nuke/Caching/DataCache.swift +++ /dev/null @@ -1,543 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// An LRU disk cache that stores data in separate files. -/// -/// ``DataCache`` uses LRU cleanup policy (least recently used items are removed -/// first). The elements stored in the cache are automatically discarded if -/// either *cost* or *count* limit is reached. The sweeps are performed periodically. -/// -/// DataCache always writes and removes data asynchronously. It also allows for -/// reading and writing data in parallel. It is implemented using a staging -/// area which stores changes until they are flushed to disk: -/// -/// ```swift -/// // Schedules data to be written asynchronously and returns immediately -/// cache[key] = data -/// -/// // The data is returned from the staging area -/// let data = cache[key] -/// -/// // Schedules data to be removed asynchronously and returns immediately -/// cache[key] = nil -/// -/// // Data is nil -/// let data = cache[key] -/// ``` -/// -/// - important: It's possible to have more than one instance of ``DataCache`` with -/// the same path but it is not recommended. -public final class DataCache: DataCaching, @unchecked Sendable { - /// Size limit in bytes. `150 Mb` by default. - /// - /// Changes to the size limit will take effect when the next LRU sweep is run. - public var sizeLimit: Int = 1024 * 1024 * 150 - - /// When performing a sweep, the cache will remote entries until the size of - /// the remaining items is lower than or equal to `sizeLimit * trimRatio` and - /// the total count is lower than or equal to `countLimit * trimRatio`. `0.7` - /// by default. - var trimRatio = 0.7 - - /// The path for the directory managed by the cache. - public let path: URL - - /// The time interval between cache sweeps. The default value is 1 hour. - public var sweepInterval: TimeInterval = 3600 - - // Deprecated in Nuke 12.2 - @available(*, deprecated, message: "It's not recommended to use compression with the popular image formats that already compress the data") - public var isCompressionEnabled: Bool { - get { _isCompressionEnabled } - set { _isCompressionEnabled = newValue } - } - var _isCompressionEnabled = false - - // Staging - - private let lock = NSLock() - private var staging = Staging() - private var isFlushNeeded = false - private var isFlushScheduled = false - - var flushInterval: DispatchTimeInterval = .seconds(1) - - private struct Metadata: Codable { - var lastSweepDate: Date? - } - - /// A queue which is used for disk I/O. - public let queue = DispatchQueue(label: "com.github.kean.Nuke.DataCache.WriteQueue", qos: .utility) - - /// A function which generates a filename for the given key. A good candidate - /// for a filename generator is a _cryptographic_ hash function like SHA1. - /// - /// The reason why filename needs to be generated in the first place is - /// that filesystems have a size limit for filenames (e.g. 255 UTF-8 characters - /// in AFPS) and do not allow certain characters to be used in filenames. - public typealias FilenameGenerator = (_ key: String) -> String? - - private let filenameGenerator: FilenameGenerator - - /// Creates a cache instance with a given `name`. The cache creates a directory - /// with the given `name` in a `.cachesDirectory` in `.userDomainMask`. - /// - parameter filenameGenerator: Generates a filename for the given URL. - /// The default implementation generates a filename using SHA1 hash function. - public convenience init(name: String, filenameGenerator: @escaping (String) -> String? = DataCache.filename(for:)) throws { - // This should be replaced with URL.cachesDirectory on iOS 16, which never fails - guard let root = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else { - throw NSError(domain: NSCocoaErrorDomain, code: NSFileNoSuchFileError, userInfo: nil) - } - try self.init(path: root.appendingPathComponent(name, isDirectory: true), filenameGenerator: filenameGenerator) - } - - /// Creates a cache instance with a given path. - /// - parameter filenameGenerator: Generates a filename for the given URL. - /// The default implementation generates a filename using SHA1 hash function. - public init(path: URL, filenameGenerator: @escaping (String) -> String? = DataCache.filename(for:)) throws { - self.path = path - self.filenameGenerator = filenameGenerator - try self.didInit() - } - - /// A `FilenameGenerator` implementation which uses SHA1 hash function to - /// generate a filename from the given key. - public static func filename(for key: String) -> String? { - key.isEmpty ? nil : key.sha1 - } - - private func didInit() throws { - try FileManager.default.createDirectory(at: path, withIntermediateDirectories: true, attributes: nil) - scheduleSweep() - } - - private func scheduleSweep() { - if let lastSweepDate = getMetadata().lastSweepDate, - Date().timeIntervalSince(lastSweepDate) < sweepInterval { - return // Already completed recently - } - // Add a bit of a delay to free the resources during launch - queue.asyncAfter(deadline: .now() + 5.0, qos: .background) { [weak self] in - self?.performSweep() - self?.updateMetadata { - $0.lastSweepDate = Date() - } - } - } - - // MARK: DataCaching - - /// Retrieves data for the given key. - public func cachedData(for key: String) -> Data? { - if let change = change(for: key) { - switch change { // Change wasn't flushed to disk yet - case let .add(data): - return data - case .remove: - return nil - } - } - guard let url = url(for: key) else { - return nil - } - return try? decompressed(Data(contentsOf: url)) - } - - /// Returns `true` if the cache contains the data for the given key. - public func containsData(for key: String) -> Bool { - if let change = change(for: key) { - switch change { // Change wasn't flushed to disk yet - case .add: - return true - case .remove: - return false - } - } - guard let url = url(for: key) else { - return false - } - return FileManager.default.fileExists(atPath: url.path) - } - - private func change(for key: String) -> Staging.ChangeType? { - lock.lock() - defer { lock.unlock() } - return staging.change(for: key) - } - - /// Stores data for the given key. The method returns instantly and the data - /// is written asynchronously. - public func storeData(_ data: Data, for key: String) { - stage { staging.add(data: data, for: key) } - } - - /// Removes data for the given key. The method returns instantly, the data - /// is removed asynchronously. - public func removeData(for key: String) { - stage { staging.removeData(for: key) } - } - - /// Removes all items. The method returns instantly, the data is removed - /// asynchronously. - public func removeAll() { - stage { staging.removeAllStagedChanges() } - } - - private func stage(_ change: () -> Void) { - lock.lock() - change() - setNeedsFlushChanges() - lock.unlock() - } - - /// Accesses the data associated with the given key for reading and writing. - /// - /// When you assign a new data for a key and the key already exists, the cache - /// overwrites the existing data. - /// - /// When assigning or removing data, the subscript adds a requested operation - /// in a staging area and returns immediately. The staging area allows for - /// reading and writing data in parallel. - /// - /// ```swift - /// // Schedules data to be written asynchronously and returns immediately - /// cache[key] = data - /// - /// // The data is returned from the staging area - /// let data = cache[key] - /// - /// // Schedules data to be removed asynchronously and returns immediately - /// cache[key] = nil - /// - /// // Data is nil - /// let data = cache[key] - /// ``` - public subscript(key: String) -> Data? { - get { - cachedData(for: key) - } - set { - if let data = newValue { - storeData(data, for: key) - } else { - removeData(for: key) - } - } - } - - // MARK: Managing URLs - - /// Uses the the filename generator that the cache was initialized with to - /// generate and return a filename for the given key. - public func filename(for key: String) -> String? { - filenameGenerator(key) - } - - /// Returns `url` for the given cache key. - public func url(for key: String) -> URL? { - guard let filename = self.filename(for: key) else { return nil } - return self.path.appendingPathComponent(filename, isDirectory: false) - } - - // MARK: Flush Changes - - /// Synchronously waits on the caller's thread until all outstanding disk I/O - /// operations are finished. - public func flush() { - queue.sync { self.flushChangesIfNeeded() } - } - - /// Synchronously waits on the caller's thread until all outstanding disk I/O - /// operations for the given key are finished. - public func flush(for key: String) { - queue.sync { - guard let change = lock.withLock({ staging.changes[key] }) else { return } - perform(change) - lock.withLock { staging.flushed(change) } - } - } - - private func setNeedsFlushChanges() { - guard !isFlushNeeded else { return } - isFlushNeeded = true - scheduleNextFlush() - } - - private func scheduleNextFlush() { - guard !isFlushScheduled else { return } - isFlushScheduled = true - queue.asyncAfter(deadline: .now() + flushInterval) { self.flushChangesIfNeeded() } - } - - private func flushChangesIfNeeded() { - // Create a snapshot of the recently made changes - let staging: Staging - lock.lock() - guard isFlushNeeded else { - return lock.unlock() - } - staging = self.staging - isFlushNeeded = false - lock.unlock() - - // Apply the snapshot to disk - performChanges(for: staging) - - // Update the staging area and schedule the next flush if needed - lock.lock() - self.staging.flushed(staging) - isFlushScheduled = false - if isFlushNeeded { - scheduleNextFlush() - } - lock.unlock() - } - - // MARK: - I/O - - private func performChanges(for staging: Staging) { - autoreleasepool { - if let change = staging.changeRemoveAll { - perform(change) - } - for change in staging.changes.values { - perform(change) - } - } - } - - private func perform(_ change: Staging.ChangeRemoveAll) { - try? FileManager.default.removeItem(at: self.path) - try? FileManager.default.createDirectory(at: self.path, withIntermediateDirectories: true, attributes: nil) - } - - /// Performs the IO for the given change. - private func perform(_ change: Staging.Change) { - guard let url = url(for: change.key) else { - return - } - switch change.type { - case let .add(data): - do { - try compressed(data).write(to: url) - } catch let error as NSError { - guard error.code == CocoaError.fileNoSuchFile.rawValue && error.domain == CocoaError.errorDomain else { return } - try? FileManager.default.createDirectory(at: self.path, withIntermediateDirectories: true, attributes: nil) - try? compressed(data).write(to: url) // re-create a directory and try again - } - case .remove: - try? FileManager.default.removeItem(at: url) - } - } - - // MARK: Compression - - private func compressed(_ data: Data) throws -> Data { - guard _isCompressionEnabled else { - return data - } - return try (data as NSData).compressed(using: .lzfse) as Data - } - - private func decompressed(_ data: Data) throws -> Data { - guard _isCompressionEnabled else { - return data - } - return try (data as NSData).decompressed(using: .lzfse) as Data - } - - // MARK: Sweep - - /// Synchronously performs a cache sweep and removes the least recently items - /// which no longer fit in cache. - public func sweep() { - queue.sync { self.performSweep() } - } - - /// Discards the least recently used items first. - private func performSweep() { - var items = contents(keys: [.contentAccessDateKey, .totalFileAllocatedSizeKey]) - guard !items.isEmpty else { - return - } - var size = items.reduce(0) { $0 + ($1.meta.totalFileAllocatedSize ?? 0) } - - guard size > sizeLimit else { - return // All good, no need to perform any work. - } - - let targetSizeLimit = Int(Double(sizeLimit) * trimRatio) - - // Most recently accessed items first - let past = Date.distantPast - items.sort { // Sort in place - ($0.meta.contentAccessDate ?? past) > ($1.meta.contentAccessDate ?? past) - } - - // Remove the items until it satisfies both size and count limits. - while size > targetSizeLimit, let item = items.popLast() { - size -= (item.meta.totalFileAllocatedSize ?? 0) - try? FileManager.default.removeItem(at: item.url) - } - } - - // MARK: Contents - - struct Entry { - let url: URL - let meta: URLResourceValues - } - - func contents(keys: [URLResourceKey] = []) -> [Entry] { - guard let urls = try? FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: keys, options: .skipsHiddenFiles) else { - return [] - } - let keys = Set(keys) - return urls.compactMap { - guard let meta = try? $0.resourceValues(forKeys: keys) else { - return nil - } - return Entry(url: $0, meta: meta) - } - } - - // MARK: Metadata - - private func getMetadata() -> Metadata { - if let data = try? Data(contentsOf: metadataFileURL), - let metadata = try? JSONDecoder().decode(Metadata.self, from: data) { - return metadata - } - return Metadata() - } - - private func updateMetadata(_ closure: (inout Metadata) -> Void) { - var metadata = getMetadata() - closure(&metadata) - try? JSONEncoder().encode(metadata).write(to: metadataFileURL) - } - - private var metadataFileURL: URL { - path.appendingPathComponent(".data-cache-info", isDirectory: false) - } - - // MARK: Inspection - - /// The total number of items in the cache. - /// - /// - important: Requires disk IO, avoid using from the main thread. - public var totalCount: Int { - contents().count - } - - /// The total file size of items written on disk. - /// - /// Uses `URLResourceKey.fileSizeKey` to calculate the size of each entry. - /// The total allocated size (see `totalAllocatedSize`. on disk might - /// actually be bigger. - /// - /// - important: Requires disk IO, avoid using from the main thread. - public var totalSize: Int { - contents(keys: [.fileSizeKey]).reduce(0) { - $0 + ($1.meta.fileSize ?? 0) - } - } - - /// The total file allocated size of all the items written on disk. - /// - /// Uses `URLResourceKey.totalFileAllocatedSizeKey`. - /// - /// - important: Requires disk IO, avoid using from the main thread. - public var totalAllocatedSize: Int { - contents(keys: [.totalFileAllocatedSizeKey]).reduce(0) { - $0 + ($1.meta.totalFileAllocatedSize ?? 0) - } - } -} - -// MARK: - Staging - -/// DataCache allows for parallel reads and writes. This is made possible by -/// DataCacheStaging. -/// -/// For example, when the data is added in cache, it is first added to staging -/// and is removed from staging only after data is written to disk. Removal works -/// the same way. -private struct Staging { - private(set) var changes = [String: Change]() - private(set) var changeRemoveAll: ChangeRemoveAll? - - struct ChangeRemoveAll { - let id: Int - } - - struct Change { - let key: String - let id: Int - let type: ChangeType - } - - enum ChangeType { - case add(Data) - case remove - } - - private var nextChangeId = 0 - - // MARK: Changes - - func change(for key: String) -> ChangeType? { - if let change = changes[key] { - return change.type - } - if changeRemoveAll != nil { - return .remove - } - return nil - } - - // MARK: Register Changes - - mutating func add(data: Data, for key: String) { - nextChangeId += 1 - changes[key] = Change(key: key, id: nextChangeId, type: .add(data)) - } - - mutating func removeData(for key: String) { - nextChangeId += 1 - changes[key] = Change(key: key, id: nextChangeId, type: .remove) - } - - mutating func removeAllStagedChanges() { - nextChangeId += 1 - changeRemoveAll = ChangeRemoveAll(id: nextChangeId) - changes.removeAll() - } - - // MARK: Flush Changes - - mutating func flushed(_ staging: Staging) { - for change in staging.changes.values { - flushed(change) - } - if let change = staging.changeRemoveAll { - flushed(change) - } - } - - mutating func flushed(_ change: Change) { - if let index = changes.index(forKey: change.key), - changes[index].value.id == change.id { - changes.remove(at: index) - } - } - - mutating func flushed(_ change: ChangeRemoveAll) { - if changeRemoveAll?.id == change.id { - changeRemoveAll = nil - } - } -} diff --git a/Nuke/Caching/DataCaching.swift b/Nuke/Caching/DataCaching.swift deleted file mode 100644 index ad1c884..0000000 --- a/Nuke/Caching/DataCaching.swift +++ /dev/null @@ -1,27 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// Data cache. -/// -/// - important: The implementation must be thread safe. -public protocol DataCaching: Sendable { - /// Retrieves data from cache for the given key. - func cachedData(for key: String) -> Data? - - /// Returns `true` if the cache contains data for the given key. - func containsData(for key: String) -> Bool - - /// Stores data for the given key. - /// - note: The implementation must return immediately and store data - /// asynchronously. - func storeData(_ data: Data, for key: String) - - /// Removes data for the given key. - func removeData(for key: String) - - /// Removes all items. - func removeAll() -} diff --git a/Nuke/Caching/ImageCache.swift b/Nuke/Caching/ImageCache.swift deleted file mode 100644 index 1530140..0000000 --- a/Nuke/Caching/ImageCache.swift +++ /dev/null @@ -1,116 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation -#if !os(macOS) -import UIKit -#else -import Cocoa -#endif - -/// An LRU memory cache. -/// -/// The elements stored in cache are automatically discarded if either *cost* or -/// *count* limit is reached. The default cost limit represents a number of bytes -/// and is calculated based on the amount of physical memory available on the -/// device. The default count limit is set to `Int.max`. -/// -/// ``ImageCache`` automatically removes all stored elements when it receives a -/// memory warning. It also automatically removes *most* stored elements -/// when the app enters the background. -public final class ImageCache: ImageCaching { - private let impl: Cache - - /// The maximum total cost that the cache can hold. - public var costLimit: Int { - get { impl.conf.costLimit } - set { impl.conf.costLimit = newValue } - } - - /// The maximum number of items that the cache can hold. - public var countLimit: Int { - get { impl.conf.countLimit } - set { impl.conf.countLimit = newValue } - } - - /// Default TTL (time to live) for each entry. Can be used to make sure that - /// the entries get validated at some point. `nil` (never expire) by default. - public var ttl: TimeInterval? { - get { impl.conf.ttl } - set { impl.conf.ttl = newValue } - } - - /// The maximum cost of an entry in proportion to the ``costLimit``. - /// By default, `0.1`. - public var entryCostLimit: Double { - get { impl.conf.entryCostLimit } - set { impl.conf.entryCostLimit = newValue } - } - - /// The total number of items in the cache. - public var totalCount: Int { impl.totalCount } - - /// The total cost of items in the cache. - public var totalCost: Int { impl.totalCost } - - /// Shared `Cache` instance. - public static let shared = ImageCache() - - /// Initializes `Cache`. - /// - parameter costLimit: Default value represents a number of bytes and is - /// calculated based on the amount of the physical memory available on the device. - /// - parameter countLimit: `Int.max` by default. - public init(costLimit: Int = ImageCache.defaultCostLimit(), countLimit: Int = Int.max) { - impl = Cache(costLimit: costLimit, countLimit: countLimit) - } - - /// Returns a cost limit computed based on the amount of the physical memory - /// available on the device. The limit is capped at 512 MB. - public static func defaultCostLimit() -> Int { - let physicalMemory = ProcessInfo.processInfo.physicalMemory - let ratio = physicalMemory <= (536_870_912 /* 512 Mb */) ? 0.1 : 0.2 - let limit = min(536_870_912, physicalMemory / UInt64(1 / ratio)) - return Int(limit) - } - - public subscript(key: ImageCacheKey) -> ImageContainer? { - get { impl.value(forKey: key) } - set { - if let image = newValue { - impl.set(image, forKey: key, cost: cost(for: image)) - } else { - impl.removeValue(forKey: key) - } - } - } - - /// Removes all cached images. - public func removeAll() { - impl.removeAllCachedValues() - } - /// Removes least recently used items from the cache until the total cost - /// of the remaining items is less than the given cost limit. - public func trim(toCost limit: Int) { - impl.trim(toCost: limit) - } - - /// Removes least recently used items from the cache until the total count - /// of the remaining items is less than the given count limit. - public func trim(toCount limit: Int) { - impl.trim(toCount: limit) - } - - /// Returns cost for the given image by approximating its bitmap size in bytes in memory. - func cost(for container: ImageContainer) -> Int { - let dataCost = container.data?.count ?? 0 - - // bytesPerRow * height gives a rough estimation of how much memory - // image uses in bytes. In practice this algorithm combined with a - // conservative default cost limit works OK. - guard let cgImage = container.image.cgImage else { - return 1 + dataCost - } - return cgImage.bytesPerRow * cgImage.height + dataCost - } -} diff --git a/Nuke/Caching/ImageCaching.swift b/Nuke/Caching/ImageCaching.swift deleted file mode 100644 index eaef111..0000000 --- a/Nuke/Caching/ImageCaching.swift +++ /dev/null @@ -1,37 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// In-memory image cache. -/// -/// The implementation must be thread safe. -public protocol ImageCaching: AnyObject, Sendable { - /// Access the image cached for the given request. - subscript(key: ImageCacheKey) -> ImageContainer? { get set } - - /// Removes all caches items. - func removeAll() -} - -/// An opaque container that acts as a cache key. -/// -/// In general, you don't construct it directly, and use ``ImagePipeline`` or ``ImagePipeline/Cache-swift.struct`` APIs. -public struct ImageCacheKey: Hashable, Sendable { - let key: Inner - - // This is faster than using AnyHashable (and it shows in performance tests). - enum Inner: Hashable, Sendable { - case custom(String) - case `default`(CacheKey) - } - - public init(key: String) { - self.key = .custom(key) - } - - public init(request: ImageRequest) { - self.key = .default(request.makeImageCacheKey()) - } -} diff --git a/Nuke/Decoding/AssetType.swift b/Nuke/Decoding/AssetType.swift deleted file mode 100644 index 2df4406..0000000 --- a/Nuke/Decoding/AssetType.swift +++ /dev/null @@ -1,89 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// A uniform type identifier (UTI). -public struct AssetType: ExpressibleByStringLiteral, Hashable, Sendable { - public let rawValue: String - - public init(rawValue: String) { - self.rawValue = rawValue - } - - public init(stringLiteral value: String) { - self.rawValue = value - } - - public static let png: AssetType = "public.png" - public static let jpeg: AssetType = "public.jpeg" - public static let gif: AssetType = "com.compuserve.gif" - /// HEIF (High Efficiency Image Format) by Apple. - public static let heic: AssetType = "public.heic" - - /// WebP - /// - /// Native decoding support only available on the following platforms: macOS 11, - /// iOS 14, watchOS 7, tvOS 14. - public static let webp: AssetType = "public.webp" - - public static let mp4: AssetType = "public.mpeg4" - - /// The M4V file format is a video container format developed by Apple and - /// is very similar to the MP4 format. The primary difference is that M4V - /// files may optionally be protected by DRM copy protection. - public static let m4v: AssetType = "public.m4v" - - public static let mov: AssetType = "public.mov" -} - -extension AssetType { - /// Determines a type of the image based on the given data. - public init?(_ data: Data) { - guard let type = AssetType.make(data) else { - return nil - } - self = type - } - - private static func make(_ data: Data) -> AssetType? { - func _match(_ numbers: [UInt8?], offset: Int = 0) -> Bool { - guard data.count >= numbers.count else { - return false - } - return zip(numbers.indices, numbers).allSatisfy { index, number in - guard let number else { return true } - guard (index + offset) < data.count else { return false } - return data[index + offset] == number - } - } - - // JPEG magic numbers https://en.wikipedia.org/wiki/JPEG - if _match([0xFF, 0xD8, 0xFF]) { return .jpeg } - - // PNG Magic numbers https://en.wikipedia.org/wiki/Portable_Network_Graphics - if _match([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) { return .png } - - // GIF magic numbers https://en.wikipedia.org/wiki/GIF - if _match([0x47, 0x49, 0x46]) { return .gif } - - // WebP magic numbers https://en.wikipedia.org/wiki/List_of_file_signatures - if _match([0x52, 0x49, 0x46, 0x46, nil, nil, nil, nil, 0x57, 0x45, 0x42, 0x50]) { return .webp } - - // see https://stackoverflow.com/questions/21879981/avfoundation-avplayer-supported-formats-no-vob-or-mpg-containers - // https://en.wikipedia.org/wiki/List_of_file_signatures - if _match([0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D], offset: 4) { return .mp4 } - - // https://www.garykessler.net/library/file_sigs.html - if _match([0x66, 0x74, 0x79, 0x70, 0x6D, 0x70, 0x34, 0x32], offset: 4) { return .m4v } - - if _match([0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x56, 0x20], offset: 4) { return .m4v } - - // MOV magic numbers https://www.garykessler.net/library/file_sigs.html - if _match([0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x20], offset: 4) { return .mov } - - // Either not enough data, or we just don't support this format. - return nil - } -} diff --git a/Nuke/Decoding/ImageDecoderRegistry.swift b/Nuke/Decoding/ImageDecoderRegistry.swift deleted file mode 100644 index 0232de8..0000000 --- a/Nuke/Decoding/ImageDecoderRegistry.swift +++ /dev/null @@ -1,72 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// A registry of image codecs. -public final class ImageDecoderRegistry: @unchecked Sendable { - /// A shared registry. - public static let shared = ImageDecoderRegistry() - - private var matches = [(ImageDecodingContext) -> (any ImageDecoding)?]() - private let lock = NSLock() - - /// Initializes a custom registry. - public init() { - register(ImageDecoders.Default.init) - } - - /// Returns a decoder that matches the given context. - public func decoder(for context: ImageDecodingContext) -> (any ImageDecoding)? { - lock.lock() - defer { lock.unlock() } - - for match in matches.reversed() { - if let decoder = match(context) { - return decoder - } - } - return nil - } - - /// Registers a decoder to be used in a given decoding context. - /// - /// **Progressive Decoding** - /// - /// The decoder is created once and is used for the entire decoding session, - /// including progressively decoded images. If the decoder doesn't support - /// progressive decoding, return `nil` when `isCompleted` is `false`. - public func register(_ match: @escaping (ImageDecodingContext) -> (any ImageDecoding)?) { - lock.lock() - defer { lock.unlock() } - - matches.append(match) - } - - /// Removes all registered decoders. - public func clear() { - lock.lock() - defer { lock.unlock() } - - matches = [] - } -} - -/// Image decoding context used when selecting which decoder to use. -public struct ImageDecodingContext: @unchecked Sendable { - public var request: ImageRequest - public var data: Data - /// Returns `true` if the download was completed. - public var isCompleted: Bool - public var urlResponse: URLResponse? - public var cacheType: ImageResponse.CacheType? - - public init(request: ImageRequest, data: Data, isCompleted: Bool, urlResponse: URLResponse?, cacheType: ImageResponse.CacheType?) { - self.request = request - self.data = data - self.isCompleted = isCompleted - self.urlResponse = urlResponse - self.cacheType = cacheType - } -} diff --git a/Nuke/Decoding/ImageDecoders+Default.swift b/Nuke/Decoding/ImageDecoders+Default.swift deleted file mode 100644 index 9cafe07..0000000 --- a/Nuke/Decoding/ImageDecoders+Default.swift +++ /dev/null @@ -1,215 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -#if !os(macOS) -import UIKit -#else -import Cocoa -#endif - -/// A namespace with all available decoders. -public enum ImageDecoders {} - -extension ImageDecoders { - - /// A decoder that supports all of the formats natively supported by the system. - /// - /// - note: The decoder automatically sets the scale of the decoded images to - /// match the scale of the screen. - /// - /// - note: The default decoder supports progressive JPEG. It produces a new - /// preview every time it encounters a new full frame. - public final class Default: ImageDecoding, @unchecked Sendable { - // Number of scans that the decoder has found so far. The last scan might be - // incomplete at this point. - var numberOfScans: Int { scanner.numberOfScans } - private var scanner = ProgressiveJPEGScanner() - - private var isPreviewForGIFGenerated = false - private var scale: CGFloat? - private var thumbnail: ImageRequest.ThumbnailOptions? - private let lock = NSLock() - - public var isAsynchronous: Bool { thumbnail != nil } - - public init() { } - - /// Returns `nil` if progressive decoding is not allowed for the given - /// content. - public init?(context: ImageDecodingContext) { - self.scale = context.request.scale.map { CGFloat($0) } - self.thumbnail = context.request.thumbnail - - if !context.isCompleted && !isProgressiveDecodingAllowed(for: context.data) { - return nil // Progressive decoding not allowed for this image - } - } - - public func decode(_ data: Data) throws -> ImageContainer { - lock.lock() - defer { lock.unlock() } - - func makeImage() -> PlatformImage? { - if let thumbnail { - return makeThumbnail(data: data, options: thumbnail) - } - return ImageDecoders.Default._decode(data, scale: scale) - } - guard let image = makeImage() else { - throw ImageDecodingError.unknown - } - let type = AssetType(data) - var container = ImageContainer(image: image) - container.type = type - if type == .gif { - container.data = data - } - if numberOfScans > 0 { - container.userInfo[.scanNumberKey] = numberOfScans - } - if thumbnail != nil { - container.userInfo[.isThumbnailKey] = true - } - return container - } - - public func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? { - lock.lock() - defer { lock.unlock() } - - let assetType = AssetType(data) - if assetType == .gif { // Special handling for GIF - if !isPreviewForGIFGenerated, let image = ImageDecoders.Default._decode(data, scale: scale) { - isPreviewForGIFGenerated = true - return ImageContainer(image: image, type: .gif, isPreview: true, userInfo: [:]) - } - return nil - } - - guard let endOfScan = scanner.scan(data), endOfScan > 0 else { - return nil - } - guard let image = ImageDecoders.Default._decode(data[0...endOfScan], scale: scale) else { - return nil - } - return ImageContainer(image: image, type: assetType, isPreview: true, userInfo: [.scanNumberKey: numberOfScans]) - } - } -} - -private func isProgressiveDecodingAllowed(for data: Data) -> Bool { - let assetType = AssetType(data) - - // Determined whether the image supports progressive decoding or not - // (only proressive JPEG is allowed for now, but you can add support - // for other formats by implementing your own decoder). - if assetType == .jpeg, ImageProperties.JPEG(data)?.isProgressive == true { - return true - } - - // Generate one preview for GIF. - if assetType == .gif { - return true - } - - return false -} - -private struct ProgressiveJPEGScanner: Sendable { - // Number of scans that the decoder has found so far. The last scan might be - // incomplete at this point. - private(set) var numberOfScans = 0 - private var lastStartOfScan: Int = 0 // Index of the last found Start of Scan - private var scannedIndex: Int = -1 // Index at which previous scan was finished - - /// Scans the given data. If finds new scans, returns the last index of the - /// last available scan. - mutating func scan(_ data: Data) -> Int? { - // Check if there is more data to scan. - guard (scannedIndex + 1) < data.count else { - return nil - } - - // Start scanning from the where it left off previous time. - var index = (scannedIndex + 1) - var numberOfScans = self.numberOfScans - while index < (data.count - 1) { - scannedIndex = index - // 0xFF, 0xDA - Start Of Scan - if data[index] == 0xFF, data[index + 1] == 0xDA { - lastStartOfScan = index - numberOfScans += 1 - } - index += 1 - } - - // Found more scans this the previous time - guard numberOfScans > self.numberOfScans else { - return nil - } - self.numberOfScans = numberOfScans - - // `> 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 numberOfScans > 1 && lastStartOfScan > 0 else { - return nil - } - - return lastStartOfScan - 1 - } -} - -extension ImageDecoders.Default { - private static func _decode(_ data: Data, scale: CGFloat?) -> PlatformImage? { -#if os(macOS) - return NSImage(data: data) -#else - return UIImage(data: data, scale: scale ?? 1) -#endif - } -} - -enum ImageProperties {} - -// Keeping this private for now, not sure neither about the API, not the implementation. -extension ImageProperties { - struct JPEG { - var isProgressive: Bool - - init?(_ data: Data) { - guard let isProgressive = ImageProperties.JPEG.isProgressive(data) else { - return nil - } - self.isProgressive = isProgressive - } - - private static func isProgressive(_ data: Data) -> Bool? { - var index = 3 // start scanning right after magic numbers - while index < (data.count - 1) { - // A example of first few bytes of progressive jpeg image: - // FF D8 FF E0 00 10 4A 46 49 46 00 01 01 00 00 48 00 ... - // - // 0xFF, 0xC0 - Start Of Frame (baseline DCT) - // 0xFF, 0xC2 - Start Of Frame (progressive DCT) - // https://en.wikipedia.org/wiki/JPEG - // - // As an alternative, Image I/O provides facilities to parse - // JPEG metadata via CGImageSourceCopyPropertiesAtIndex. It is a - // bit too convoluted to use and most likely slightly less - // efficient that checking this one special bit directly. - if data[index] == 0xFF { - if data[index + 1] == 0xC2 { - return true - } - if data[index + 1] == 0xC0 { - return false // baseline - } - } - index += 1 - } - return nil - } - } -} diff --git a/Nuke/Decoding/ImageDecoders+Empty.swift b/Nuke/Decoding/ImageDecoders+Empty.swift deleted file mode 100644 index 4097299..0000000 --- a/Nuke/Decoding/ImageDecoders+Empty.swift +++ /dev/null @@ -1,36 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -extension ImageDecoders { - /// A decoder that returns an empty placeholder image and attaches image - /// data to the image container. - public struct Empty: ImageDecoding, Sendable { - public let isProgressive: Bool - private let assetType: AssetType? - - public var isAsynchronous: Bool { false } - - /// Initializes the decoder. - /// - /// - Parameters: - /// - type: Image type to be associated with an image container. - /// `nil` by default. - /// - isProgressive: If `false`, returns nil for every progressive - /// scan. `false` by default. - public init(assetType: AssetType? = nil, isProgressive: Bool = false) { - self.assetType = assetType - self.isProgressive = isProgressive - } - - public func decode(_ data: Data) throws -> ImageContainer { - ImageContainer(image: PlatformImage(), type: assetType, data: data, userInfo: [:]) - } - - public func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? { - isProgressive ? ImageContainer(image: PlatformImage(), type: assetType, data: data, userInfo: [:]) : nil - } - } -} diff --git a/Nuke/Decoding/ImageDecoding.swift b/Nuke/Decoding/ImageDecoding.swift deleted file mode 100644 index a33fa9b..0000000 --- a/Nuke/Decoding/ImageDecoding.swift +++ /dev/null @@ -1,64 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// An image decoder. -/// -/// A decoder is a one-shot object created for a single image decoding session. -/// -/// - note: If you need additional information in the decoder, you can pass -/// anything that you might need from the ``ImageDecodingContext``. -public protocol ImageDecoding: Sendable { - /// Return `true` if you want the decoding to be performed on the decoding - /// queue (see ``ImagePipeline/Configuration-swift.struct/imageDecodingQueue``). If `false`, the decoding will be - /// performed synchronously on the pipeline operation queue. By default, `true`. - var isAsynchronous: Bool { get } - - /// Produces an image from the given image data. - func decode(_ data: Data) throws -> ImageContainer - - /// Produces an image from the given partially downloaded image data. - /// This method might be called multiple times during a single decoding - /// session. When the image download is complete, ``decode(_:)`` method is called. - /// - /// - returns: nil by default. - func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? -} - -extension ImageDecoding { - /// Returns `true` by default. - public var isAsynchronous: Bool { true } - - /// The default implementation which simply returns `nil` (no progressive - /// decoding available). - public func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? { nil } -} - -public enum ImageDecodingError: Error, CustomStringConvertible, Sendable { - case unknown - - public var description: String { "Unknown" } -} - -extension ImageDecoding { - func decode(_ context: ImageDecodingContext) throws -> ImageResponse { - let container: ImageContainer = try autoreleasepool { - if context.isCompleted { - return try decode(context.data) - } else { - if let preview = decodePartiallyDownloadedData(context.data) { - return preview - } - throw ImageDecodingError.unknown - } - } -#if !os(macOS) - if container.userInfo[.isThumbnailKey] == nil { - ImageDecompression.setDecompressionNeeded(true, for: container.image) - } -#endif - return ImageResponse(container: container, request: context.request, urlResponse: context.urlResponse, cacheType: context.cacheType) - } -} diff --git a/Nuke/Encoding/ImageEncoders+Default.swift b/Nuke/Encoding/ImageEncoders+Default.swift deleted file mode 100644 index bfeba48..0000000 --- a/Nuke/Encoding/ImageEncoders+Default.swift +++ /dev/null @@ -1,45 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -#if !os(macOS) -import UIKit -#else -import AppKit -#endif - -extension ImageEncoders { - /// A default adaptive encoder which uses best encoder available depending - /// on the input image and its configuration. - public struct Default: ImageEncoding { - public var compressionQuality: Float - - /// Set to `true` to switch to HEIF when it is available on the current hardware. - /// `false` by default. - public var isHEIFPreferred = false - - public init(compressionQuality: Float = 0.8) { - self.compressionQuality = compressionQuality - } - - public func encode(_ image: PlatformImage) -> Data? { - guard let cgImage = image.cgImage else { - return nil - } - let type: AssetType - if cgImage.isOpaque { - if isHEIFPreferred && ImageEncoders.ImageIO.isSupported(type: .heic) { - type = .heic - } else { - type = .jpeg - } - } else { - type = .png - } - let encoder = ImageEncoders.ImageIO(type: type, compressionRatio: compressionQuality) - return encoder.encode(image) - } - } -} diff --git a/Nuke/Encoding/ImageEncoders+ImageIO.swift b/Nuke/Encoding/ImageEncoders+ImageIO.swift deleted file mode 100644 index b4f1aa5..0000000 --- a/Nuke/Encoding/ImageEncoders+ImageIO.swift +++ /dev/null @@ -1,66 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation -import CoreGraphics -import ImageIO - -#if !os(macOS) -import UIKit -#else -import AppKit -#endif - -extension ImageEncoders { - /// An Image I/O based encoder. - /// - /// Image I/O is a system framework that allows applications to read and - /// write most image file formats. This framework offers high efficiency, - /// color management, and access to image metadata. - public struct ImageIO: ImageEncoding { - public let type: AssetType - public let compressionRatio: Float - - /// - parameter format: The output format. Make sure that the format is - /// supported on the current hardware.s - /// - parameter compressionRatio: 0.8 by default. - public init(type: AssetType, compressionRatio: Float = 0.8) { - self.type = type - self.compressionRatio = compressionRatio - } - - @Atomic private static var availability = [AssetType: Bool]() - - /// Returns `true` if the encoding is available for the given format on - /// the current hardware. Some of the most recent formats might not be - /// available so its best to check before using them. - public static func isSupported(type: AssetType) -> Bool { - if let isAvailable = availability[type] { - return isAvailable - } - let isAvailable = CGImageDestinationCreateWithData( - NSMutableData() as CFMutableData, type.rawValue as CFString, 1, nil - ) != nil - availability[type] = isAvailable - return isAvailable - } - - public func encode(_ image: PlatformImage) -> Data? { - let data = NSMutableData() - var options: [CFString: Any] = [ - kCGImageDestinationLossyCompressionQuality: compressionRatio - ] -#if canImport(UIKit) - options[kCGImagePropertyOrientation] = CGImagePropertyOrientation(image.imageOrientation).rawValue -#endif - guard let source = image.cgImage, - let destination = CGImageDestinationCreateWithData(data as CFMutableData, type.rawValue as CFString, 1, nil) else { - return nil - } - CGImageDestinationAddImage(destination, source, options as CFDictionary) - CGImageDestinationFinalize(destination) - return data as Data - } - } -} diff --git a/Nuke/Encoding/ImageEncoders.swift b/Nuke/Encoding/ImageEncoders.swift deleted file mode 100644 index 51b30c1..0000000 --- a/Nuke/Encoding/ImageEncoders.swift +++ /dev/null @@ -1,20 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// A namespace with all available encoders. -public enum ImageEncoders {} - -extension ImageEncoding where Self == ImageEncoders.Default { - public static func `default`(compressionQuality: Float = 0.8) -> ImageEncoders.Default { - ImageEncoders.Default(compressionQuality: compressionQuality) - } -} - -extension ImageEncoding where Self == ImageEncoders.ImageIO { - public static func imageIO(type: AssetType, compressionRatio: Float = 0.8) -> ImageEncoders.ImageIO { - ImageEncoders.ImageIO(type: type, compressionRatio: compressionRatio) - } -} diff --git a/Nuke/Encoding/ImageEncoding.swift b/Nuke/Encoding/ImageEncoding.swift deleted file mode 100644 index 1385ea2..0000000 --- a/Nuke/Encoding/ImageEncoding.swift +++ /dev/null @@ -1,40 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -#if canImport(UIKit) -import UIKit -#endif - -#if canImport(AppKit) -import AppKit -#endif - -import ImageIO - -// MARK: - ImageEncoding - -/// An image encoder. -public protocol ImageEncoding: Sendable { - /// Encodes the given image. - func encode(_ image: PlatformImage) -> Data? - - /// An optional method which encodes the given image container. - func encode(_ container: ImageContainer, context: ImageEncodingContext) -> Data? -} - -extension ImageEncoding { - public func encode(_ container: ImageContainer, context: ImageEncodingContext) -> Data? { - if container.type == .gif { - return container.data - } - return self.encode(container.image) - } -} - -/// Image encoding context used when selecting which encoder to use. -public struct ImageEncodingContext: @unchecked Sendable { - public let request: ImageRequest - public let image: PlatformImage - public let urlResponse: URLResponse? -} diff --git a/Nuke/ImageContainer.swift b/Nuke/ImageContainer.swift deleted file mode 100644 index 346a506..0000000 --- a/Nuke/ImageContainer.swift +++ /dev/null @@ -1,132 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -#if !os(watchOS) -import AVKit -#endif - -import Foundation - -#if !os(macOS) -import UIKit.UIImage -/// Alias for `UIImage`. -public typealias PlatformImage = UIImage -#else -import AppKit.NSImage -/// Alias for `NSImage`. -public typealias PlatformImage = NSImage -#endif - -/// An image container with an image and associated metadata. -public struct ImageContainer: @unchecked Sendable { -#if os(macOS) - /// A fetched image. - public var image: NSImage { - get { ref.image } - set { mutate { $0.image = newValue } } - } -#else - /// A fetched image. - public var image: UIImage { - get { ref.image } - set { mutate { $0.image = newValue } } - } -#endif - - /// An image type. - public var type: AssetType? { - get { ref.type } - set { mutate { $0.type = newValue } } - } - - /// Returns `true` if the image in the container is a preview of the image. - public var isPreview: Bool { - get { ref.isPreview } - set { mutate { $0.isPreview = newValue } } - } - - /// Contains the original image `data`, but only if the decoder decides to - /// attach it to the image. - /// - /// The default decoder (``ImageDecoders/Default``) attaches data to GIFs to - /// allow to display them using a rendering engine of your choice. - /// - /// - note: The `data`, along with the image container itself gets stored - /// in the memory cache. - public var data: Data? { - get { ref.data } - set { mutate { $0.data = newValue } } - } - - /// An metadata provided by the user. - public var userInfo: [UserInfoKey: Any] { - get { ref.userInfo } - set { mutate { $0.userInfo = newValue } } - } - - private var ref: Container - - /// Initializes the container with the given image. - public init(image: PlatformImage, type: AssetType? = nil, isPreview: Bool = false, data: Data? = nil, userInfo: [UserInfoKey: Any] = [:]) { - self.ref = Container(image: image, type: type, isPreview: isPreview, data: data, userInfo: userInfo) - } - - func map(_ closure: (PlatformImage) throws -> PlatformImage) rethrows -> ImageContainer { - var copy = self - copy.image = try closure(image) - return copy - } - - /// A key use in ``userInfo``. - public struct UserInfoKey: Hashable, ExpressibleByStringLiteral, Sendable { - public let rawValue: String - - public init(_ rawValue: String) { - self.rawValue = rawValue - } - - public init(stringLiteral value: String) { - self.rawValue = value - } - - // For internal purposes. - static let isThumbnailKey: UserInfoKey = "com.github/kean/nuke/skip-decompression" - - /// A user info key to get the scan number (Int). - public static let scanNumberKey: UserInfoKey = "com.github/kean/nuke/scan-number" - } - - // MARK: - Copy-on-Write - - private mutating func mutate(_ closure: (Container) -> Void) { - if !isKnownUniquelyReferenced(&ref) { - ref = Container(ref) - } - closure(ref) - } - - private final class Container: @unchecked Sendable { - var image: PlatformImage - var type: AssetType? - var isPreview: Bool - var data: Data? - var userInfo: [UserInfoKey: Any] - - init(image: PlatformImage, type: AssetType?, isPreview: Bool, data: Data? = nil, userInfo: [UserInfoKey: Any]) { - self.image = image - self.type = type - self.isPreview = isPreview - self.data = data - self.userInfo = userInfo - } - - init(_ ref: Container) { - self.image = ref.image - self.type = ref.type - self.isPreview = ref.isPreview - self.data = ref.data - self.userInfo = ref.userInfo - } - } -} diff --git a/Nuke/ImageRequest.swift b/Nuke/ImageRequest.swift deleted file mode 100644 index 9cac2b6..0000000 --- a/Nuke/ImageRequest.swift +++ /dev/null @@ -1,541 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation -import Combine -import CoreGraphics - -#if canImport(UIKit) -import UIKit -#endif - -#if canImport(AppKit) -import AppKit -#endif - -/// Represents an image request that specifies what images to download, how to -/// process them, set the request priority, and more. -/// -/// Creating a request: -/// -/// ```swift -/// let request = ImageRequest( -/// url: URL(string: "http://example.com/image.jpeg"), -/// processors: [.resize(width: 320)], -/// priority: .high, -/// options: [.reloadIgnoringCachedData] -/// ) -/// let image = try await pipeline.image(for: request) -/// ``` -public struct ImageRequest: CustomStringConvertible, Sendable, ExpressibleByStringLiteral { - // MARK: Options - - /// The relative priority of the request. The priority affects the order in - /// which the requests are performed. ``Priority-swift.enum/normal`` by default. - /// - /// - note: You can change the priority of a running task using ``ImageTask/priority``. - public var priority: Priority { - get { ref.priority } - set { mutate { $0.priority = newValue } } - } - - /// Processor to be applied to the image. Empty by default. - /// - /// See to learn more. - public var processors: [any ImageProcessing] { - get { ref.processors } - set { mutate { $0.processors = newValue } } - } - - /// The request options. For a complete list of options, see ``ImageRequest/Options-swift.struct``. - 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 } } - } - - // MARK: Instance Properties - - /// 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 requests, it's an - /// image URL. For an async function – a custom ID provided in initializer. - public var imageId: String? { ref.originalImageId } - - /// Returns a debug request description. - public var description: String { - "ImageRequest(resource: \(ref.resource), priority: \(priority), processors: \(processors), options: \(options), userInfo: \(userInfo))" - } - - // MARK: Initializers - - /// Initializes the request with the given string. - public init(stringLiteral value: String) { - self.init(url: URL(string: value)) - } - - /// Initializes a request with the given `URL`. - /// - /// - parameters: - /// - url: The request URL. - /// - processors: Processors to be apply to the image. See to learn more. - /// - priority: The priority of the request. - /// - options: Image loading options. - /// - userInfo: Custom info passed alongside the request. - /// - /// ```swift - /// let request = ImageRequest( - /// url: URL(string: "http://..."), - /// processors: [.resize(size: imageView.bounds.size)], - /// priority: .high - /// ) - /// ``` - public init( - url: URL?, - processors: [any 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 `URLRequest`. - /// - /// - parameters: - /// - urlRequest: The URLRequest describing the image request. - /// - processors: Processors to be apply to the image. See to learn more. - /// - priority: The priority of the request. - /// - options: Image loading options. - /// - userInfo: Custom info passed alongside the request. - /// - /// ```swift - /// let request = ImageRequest( - /// url: URLRequest(url: URL(string: "http://...")), - /// processors: [.resize(size: imageView.bounds.size)], - /// priority: .high - /// ) - /// ``` - public init( - urlRequest: URLRequest, - processors: [any 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 async function. - /// - /// For example, you can use it with the Photos framework after wrapping its - /// API in an async function. - /// - /// ```swift - /// ImageRequest( - /// id: asset.localIdentifier, - /// data: { try await PHAssetManager.default.imageData(for: asset) } - /// ) - /// ``` - /// - /// - important: If you are using a pipeline with a custom configuration that - /// enables aggressive disk cache, fetched data will be stored in this cache. - /// You can use ``Options-swift.struct/disableDiskCache`` to disable it. - /// - /// - note: If the resource is identifiable with a `URL`, consider - /// implementing a custom data loader instead. See . - /// - /// - parameters: - /// - id: Uniquely identifies the fetched image. - /// - data: An async function to be used to fetch image data. - /// - processors: Processors to be apply to the image. See to learn more. - /// - priority: The priority of the request. - /// - options: Image loading options. - /// - userInfo: Custom info passed alongside the request. - public init( - id: String, - data: @Sendable @escaping () async throws -> Data, - processors: [any ImageProcessing] = [], - priority: Priority = .normal, - options: Options = [], - userInfo: [UserInfoKey: Any]? = nil - ) { - // It could technically be implemented without any special change to the - // pipeline by using a custom DataLoader and passing an async function in - // the request userInfo. g - self.ref = Container( - resource: .publisher(DataPublisher(id: id, data)), - processors: processors, - priority: priority, - options: options, - userInfo: userInfo - ) - } - - /// Initializes a request with the given data publisher. - /// - /// For example, here is how you can use it with the Photos framework (the - /// `imageDataPublisher` API is a custom convenience extension not included - /// in the framework). - /// - /// ```swift - /// let request = ImageRequest( - /// id: asset.localIdentifier, - /// dataPublisher: PHAssetManager.imageDataPublisher(for: asset) - /// ) - /// ``` - /// - /// - important: If you are using a pipeline with a custom configuration that - /// enables aggressive disk cache, fetched data will be stored in this cache. - /// You can use ``Options-swift.struct/disableDiskCache`` to disable it. - /// - /// - parameters: - /// - id: Uniquely identifies the fetched image. - /// - data: A data publisher to be used for fetching image data. - /// - processors: Processors to be apply to the image. See to learn more. - /// - priority: The priority of the request, ``Priority-swift.enum/normal`` by default. - /// - options: Image loading options. - /// - userInfo: Custom info passed alongside the request. - public init

( - id: String, - dataPublisher: P, - processors: [any 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 and passing a publisher in the - // request userInfo. - self.ref = Container( - resource: .publisher(DataPublisher(id: id, dataPublisher)), - processors: processors, - priority: priority, - options: options, - userInfo: userInfo - ) - } - - // MARK: Nested Types - - /// The priority affecting the order in which the requests are performed. - public enum Priority: Int, Comparable, Sendable { - case veryLow = 0, low, normal, high, veryHigh - - public static func < (lhs: Priority, rhs: Priority) -> Bool { - lhs.rawValue < rhs.rawValue - } - } - - /// Image request options. - /// - /// By default, the pipeline makes full use of all of its caching layers. You can change this behavior using options. For example, you can ignore local caches using ``ImageRequest/Options-swift.struct/reloadIgnoringCachedData`` option. - /// - /// ```swift - /// request.options = [.reloadIgnoringCachedData] - /// ``` - /// - /// Another useful cache policy is ``ImageRequest/Options-swift.struct/returnCacheDataDontLoad`` - /// that terminates the request if no cached data is available. - public struct Options: OptionSet, Hashable, Sendable { - /// Returns a raw value. - public let rawValue: UInt16 - - /// Initializes options with a given raw values. - public init(rawValue: UInt16) { - self.rawValue = rawValue - } - - /// Disables memory cache reads (see ``ImageCaching``). - public static let disableMemoryCacheReads = Options(rawValue: 1 << 0) - - /// Disables memory cache writes (see ``ImageCaching``). - public static let disableMemoryCacheWrites = Options(rawValue: 1 << 1) - - /// Disables both memory cache reads and writes (see ``ImageCaching``). - public static let disableMemoryCache: Options = [.disableMemoryCacheReads, .disableMemoryCacheWrites] - - /// Disables disk cache reads (see ``DataCaching``). - public static let disableDiskCacheReads = Options(rawValue: 1 << 2) - - /// Disables disk cache writes (see ``DataCaching``). - public static let disableDiskCacheWrites = Options(rawValue: 1 << 3) - - /// Disables both disk cache reads and writes (see ``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) - - /// Skip decompression ("bitmapping") for the given image. Decompression - /// will happen lazily when you display the image. - public static let skipDecompression = Options(rawValue: 1 << 5) - - /// Perform data loading immediately, ignoring ``ImagePipeline/Configuration-swift.struct/dataLoadingQueue``. It - /// can be used to elevate priority of certain tasks. - /// - /// - important: If there is an outstanding task for loading the same - /// resource but without this option, a new task will be created. - public static let skipDataLoadingQueue = Options(rawValue: 1 << 6) - } - - /// A key used in `userInfo` for providing custom request options. - /// - /// There are a couple of built-in options that are passed using user info - /// as well, including ``imageIdKey``, ``scaleKey``, and ``thumbnailKey``. - public struct UserInfoKey: Hashable, ExpressibleByStringLiteral, Sendable { - /// Returns a key raw value. - public let rawValue: String - - /// Initializes the key with a raw value. - public init(_ rawValue: String) { - self.rawValue = rawValue - } - - /// Initializes the key with a raw value. - public init(stringLiteral value: String) { - self.rawValue = value - } - - /// Overrides the image identifier used for caching and task coalescing. - /// - /// By default, ``ImagePipeline`` uses an image URL as a unique identifier - /// for caching and task coalescing. You can override this behavior by - /// providing a custom identifier. For example, you can use it to remove - /// transient query parameters from the URL, like access token. - /// - /// ```swift - /// 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 retrieve 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" - } - - /// Thumbnail options. - /// - /// For more info, see https://developer.apple.com/documentation/imageio/cgimagesource/image_source_option_dictionary_keys - public struct ThumbnailOptions: Hashable, Sendable { - enum TargetSize: Hashable { - case fixed(Float) - case flexible(size: ImageTargetSize, contentMode: ImageProcessingOptions.ContentMode) - - var parameters: String { - switch self { - case .fixed(let size): - return "maxPixelSize=\(size)" - case let .flexible(size, contentMode): - return "width=\(size.cgSize.width),height=\(size.cgSize.height),contentMode=\(contentMode)" - } - } - } - - let targetSize: TargetSize - - /// 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 size. - 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 size. - public var createThumbnailFromImageAlways = true - - /// Whether the thumbnail should be rotated and scaled according to the - /// orientation and pixel aspect ratio of the full image. - public var createThumbnailWithTransform = true - - /// Specifies whether image decoding and caching should happen at image - /// creation time. - public var shouldCacheImmediately = true - - /// Initializes the options with the given pixel size. The thumbnail is - /// resized to fit the target size. - /// - /// This option performs slightly faster than ``ImageRequest/ThumbnailOptions/init(size:unit:contentMode:)`` - /// because it doesn't need to read the image size. - public init(maxPixelSize: Float) { - self.targetSize = .fixed(maxPixelSize) - } - - /// Initializes the options with the given size. - /// - /// - parameters: - /// - size: The target size. - /// - unit: Unit of the target size. - /// - contentMode: A target content mode. - public init(size: CGSize, unit: ImageProcessingOptions.Unit = .points, contentMode: ImageProcessingOptions.ContentMode = .aspectFill) { - self.targetSize = .flexible(size: ImageTargetSize(size: size, unit: unit), contentMode: contentMode) - } - - /// Generates a thumbnail from the given image data. - public func makeThumbnail(with data: Data) -> PlatformImage? { - FasterImage.makeThumbnail(data: data, options: self) - } - - var identifier: String { - "com.github/kean/nuke/thumbnail?\(targetSize.parameters),options=\(createThumbnailFromImageIfAbsent)\(createThumbnailFromImageAlways)\(createThumbnailWithTransform)\(shouldCacheImmediately)" - } - } - - // MARK: Internal - - private var ref: Container - - private mutating func mutate(_ closure: (Container) -> Void) { - if !isKnownUniquelyReferenced(&ref) { - ref = Container(ref) - } - closure(ref) - } - - var resource: Resource { ref.resource } - - func withProcessors(_ processors: [any 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 thumbnail: ThumbnailOptions? { - ref.userInfo?[.thumbnailKey] as? ThumbnailOptions - } - - var scale: Float? { - (ref.userInfo?[.scaleKey] as? NSNumber)?.floatValue - } - - var publisher: DataPublisher? { - if case .publisher(let publisher) = ref.resource { return publisher } - return nil - } -} - -// MARK: - ImageRequest (Private) - -extension ImageRequest { - /// Just like many Swift built-in types, ``ImageRequest`` uses CoW approach to - /// avoid memberwise retain/releases when ``ImageRequest`` is passed around. - private final class Container: @unchecked Sendable { - // It's beneficial 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 - var priority: Priority - var options: Options - var originalImageId: String? - var processors: [any ImageProcessing] - var userInfo: [UserInfoKey: Any]? - // After trimming the request size in Nuke 10, CoW it is no longer as - // beneficial, but there still is a measurable difference. - - /// Creates a resource with a default processor. - init(resource: Resource, processors: [any ImageProcessing], priority: Priority, options: Options, userInfo: [UserInfoKey: Any]?) { - self.resource = resource - self.processors = processors - self.priority = priority - self.options = options - self.originalImageId = resource.imageId - self.userInfo = userInfo - } - - /// Creates a copy. - init(_ ref: Container) { - self.resource = ref.resource - self.processors = ref.processors - self.priority = ref.priority - self.options = ref.options - self.originalImageId = ref.originalImageId - self.userInfo = ref.userInfo - } - } - - // 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)" - } - } - - var imageId: String? { - switch self { - case .url(let url): return url?.absoluteString - case .urlRequest(let urlRequest): return urlRequest.url?.absoluteString - case .publisher(let publisher): return publisher.id - } - } - } -} diff --git a/Nuke/ImageResponse.swift b/Nuke/ImageResponse.swift deleted file mode 100644 index 0ce5111..0000000 --- a/Nuke/ImageResponse.swift +++ /dev/null @@ -1,55 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -#if !os(macOS) -import UIKit.UIImage -#else -import AppKit.NSImage -#endif - -/// An image response that contains a fetched image and some metadata. -public struct ImageResponse: @unchecked Sendable { - /// An image container with an image and associated metadata. - public var container: ImageContainer - -#if os(macOS) - /// A convenience computed property that returns an image from the container. - public var image: NSImage { container.image } -#else - /// A convenience computed property that returns an image from the container. - public var image: UIImage { container.image } -#endif - - /// Returns `true` if the image in the container is a preview of the image. - public var isPreview: Bool { container.isPreview } - - /// The request for which the response was created. - public var request: ImageRequest - - /// A response. `nil` unless the resource was fetched from the network or an - /// HTTP cache. - public var urlResponse: URLResponse? - - /// Contains a cache type in case the image was returned from one of the - /// pipeline caches (not including any of the HTTP caches if enabled). - public var cacheType: CacheType? - - /// Initializes the response with the given image. - public init(container: ImageContainer, request: ImageRequest, urlResponse: URLResponse? = nil, cacheType: CacheType? = nil) { - self.container = container - self.request = request - self.urlResponse = urlResponse - self.cacheType = cacheType - } - - /// A cache type. - public enum CacheType: Sendable { - /// Memory cache (see ``ImageCaching``) - case memory - /// Disk cache (see ``DataCaching``) - case disk - } -} diff --git a/Nuke/ImageTask.swift b/Nuke/ImageTask.swift deleted file mode 100644 index f4d3961..0000000 --- a/Nuke/ImageTask.swift +++ /dev/null @@ -1,145 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// A task performed by the ``ImagePipeline``. -/// -/// The pipeline maintains a strong reference to the task until the request -/// finishes or fails; you do not need to maintain a reference to the task unless -/// it is useful for your app. -public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Sendable { - /// An identifier that uniquely identifies the task within a given pipeline. - /// Unique only within that pipeline. - public let taskId: Int64 - - /// The original request. - public let request: ImageRequest - - /// Updates the priority of the task, even if it is already running. - public var priority: ImageRequest.Priority { - get { sync { _priority } } - set { - let didChange: Bool = sync { - guard _priority != newValue else { return false } - _priority = newValue - return _state == .running - } - guard didChange else { return } - pipeline?.imageTaskUpdatePriorityCalled(self, priority: newValue) - } - } - private var _priority: ImageRequest.Priority - - /// Returns the current download progress. Returns zeros before the download - /// is started and the expected size of the resource is known. - public internal(set) var progress: Progress { - get { sync { _progress } } - set { sync { _progress = newValue } } - } - private var _progress = Progress(completed: 0, total: 0) - - /// The download progress. - public struct Progress: Hashable, Sendable { - /// The number of bytes that the task has received. - public let completed: Int64 - /// A best-guess upper bound on the number of bytes of the resource. - public let total: Int64 - - /// Returns the fraction of the completion. - public var fraction: Float { - guard total > 0 else { return 0 } - return min(1, Float(completed) / Float(total)) - } - - /// Initializes progress with the given status. - public init(completed: Int64, total: Int64) { - self.completed = completed - self.total = total - } - } - - /// The current state of the task. - public var state: State { sync { _state } } - private var _state: State = .running - - /// The state of the image task. - public enum State { - /// The task is currently running. - case running - /// The task has received a cancel message. - case cancelled - /// The task has completed (without being canceled). - case completed - } - - var onCancel: (() -> Void)? - - weak var pipeline: ImagePipeline? - var callbackQueue: DispatchQueue? - var isDataTask = false - - /// Using it without a wrapper to reduce the number of allocations. - private let lock: os_unfair_lock_t - - deinit { - lock.deinitialize(count: 1) - lock.deallocate() - } - - init(taskId: Int64, request: ImageRequest) { - self.taskId = taskId - self.request = request - self._priority = request.priority - - lock = .allocate(capacity: 1) - lock.initialize(to: os_unfair_lock()) - } - - /// Marks task as being cancelled. - /// - /// The pipeline will immediately cancel any work associated with a task - /// unless there is an equivalent outstanding task running. - public func cancel() { - os_unfair_lock_lock(lock) - guard _state == .running else { - return os_unfair_lock_unlock(lock) - } - _state = .cancelled - os_unfair_lock_unlock(lock) - - pipeline?.imageTaskCancelCalled(self) - } - - func didComplete() { - os_unfair_lock_lock(lock) - guard _state == .running else { - return os_unfair_lock_unlock(lock) - } - _state = .completed - os_unfair_lock_unlock(lock) - } - - private func sync(_ closure: () -> T) -> T { - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - return closure() - } - - // MARK: Hashable - - public func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(self).hashValue) - } - - public static func == (lhs: ImageTask, rhs: ImageTask) -> Bool { - ObjectIdentifier(lhs) == ObjectIdentifier(rhs) - } - - // MARK: CustomStringConvertible - - public var description: String { - "ImageTask(id: \(taskId), priority: \(_priority), progress: \(progress.completed) / \(progress.total), state: \(state))" - } -} diff --git a/Nuke/Internal/Atomic.swift b/Nuke/Internal/Atomic.swift deleted file mode 100644 index a7f4a65..0000000 --- a/Nuke/Internal/Atomic.swift +++ /dev/null @@ -1,38 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -@propertyWrapper final class Atomic { - private var value: T - private let lock: os_unfair_lock_t - - init(wrappedValue value: T) { - self.value = value - self.lock = .allocate(capacity: 1) - self.lock.initialize(to: os_unfair_lock()) - } - - deinit { - lock.deinitialize(count: 1) - lock.deallocate() - } - - var wrappedValue: T { - get { getValue() } - set { setValue(newValue) } - } - - private func getValue() -> T { - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - return value - } - - private func setValue(_ newValue: T) { - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - value = newValue - } -} diff --git a/Nuke/Internal/DataPublisher.swift b/Nuke/Internal/DataPublisher.swift deleted file mode 100644 index b777f81..0000000 --- a/Nuke/Internal/DataPublisher.swift +++ /dev/null @@ -1,54 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation -@preconcurrency import Combine - -final class DataPublisher { - let id: String - private let _sink: (@escaping ((PublisherCompletion) -> Void), @escaping ((Data) -> Void)) -> any Cancellable - - init(id: String, _ publisher: P) where P.Output == Data { - self.id = id - self._sink = { onCompletion, onValue in - let cancellable = publisher.sink(receiveCompletion: { - switch $0 { - case .finished: onCompletion(.finished) - case .failure(let error): onCompletion(.failure(error)) - } - }, receiveValue: { - onValue($0) - }) - return AnonymousCancellable { cancellable.cancel() } - } - } - - convenience init(id: String, _ data: @Sendable @escaping () async throws -> Data) { - self.init(id: id, publisher(from: data)) - } - - func sink(receiveCompletion: @escaping ((PublisherCompletion) -> Void), receiveValue: @escaping ((Data) -> Void)) -> any Cancellable { - _sink(receiveCompletion, receiveValue) - } -} - -private func publisher(from closure: @Sendable @escaping () async throws -> Data) -> AnyPublisher { - Deferred { - Future { promise in - Task { - do { - let data = try await closure() - promise(.success(data)) - } catch { - promise(.failure(error)) - } - } - } - }.eraseToAnyPublisher() -} - -enum PublisherCompletion { - case finished - case failure(Error) -} diff --git a/Nuke/Internal/Extensions.swift b/Nuke/Internal/Extensions.swift deleted file mode 100644 index 03b46dc..0000000 --- a/Nuke/Internal/Extensions.swift +++ /dev/null @@ -1,63 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation -import CryptoKit - -extension String { - /// Calculates SHA1 from the given string and returns its hex representation. - /// - /// ```swift - /// print("http://test.com".sha1) - /// // prints "50334ee0b51600df6397ce93ceed4728c37fee4e" - /// ``` - var sha1: String? { - guard let input = self.data(using: .utf8) else { - return nil // The conversion to .utf8 should never fail - } - let digest = Insecure.SHA1.hash(data: input) - var output = "" - for byte in digest { - output.append(String(format: "%02x", byte)) - } - return output - } -} - -extension URL { - var isLocalResource: Bool { - scheme == "file" || scheme == "data" - } -} - -extension OperationQueue { - convenience init(maxConcurrentCount: Int) { - self.init() - self.maxConcurrentOperationCount = maxConcurrentCount - } -} - -extension ImageRequest.Priority { - var taskPriority: TaskPriority { - switch self { - case .veryLow: return .veryLow - case .low: return .low - case .normal: return .normal - case .high: return .high - case .veryHigh: return .veryHigh - } - } -} - -final class AnonymousCancellable: Cancellable { - let onCancel: @Sendable () -> Void - - init(_ onCancel: @Sendable @escaping () -> Void) { - self.onCancel = onCancel - } - - func cancel() { - onCancel() - } -} diff --git a/Nuke/Internal/Graphics.swift b/Nuke/Internal/Graphics.swift deleted file mode 100644 index 4e06db8..0000000 --- a/Nuke/Internal/Graphics.swift +++ /dev/null @@ -1,392 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -#if os(watchOS) -import ImageIO -import CoreGraphics -import WatchKit.WKInterfaceDevice -#endif - -#if canImport(UIKit) - import UIKit - #endif - - #if canImport(AppKit) - import AppKit - #endif - -extension PlatformImage { - var processed: ImageProcessingExtensions { - ImageProcessingExtensions(image: self) - } -} - -struct ImageProcessingExtensions { - let image: PlatformImage - - func byResizing(to targetSize: CGSize, - contentMode: ImageProcessingOptions.ContentMode, - upscale: Bool) -> PlatformImage? { - guard let cgImage = image.cgImage else { - return nil - } -#if canImport(UIKit) - let targetSize = targetSize.rotatedForOrientation(image.imageOrientation) -#endif - let scale = cgImage.size.getScale(targetSize: targetSize, contentMode: contentMode) - guard scale < 1 || upscale else { - return image // The image doesn't require scaling - } - let size = cgImage.size.scaled(by: scale).rounded() - return image.draw(inCanvasWithSize: size) - } - - /// Crops the input image to the given size and resizes it if needed. - /// - note: this method will always upscale. - func byResizingAndCropping(to targetSize: CGSize) -> PlatformImage? { - guard let cgImage = image.cgImage else { - return nil - } -#if canImport(UIKit) - let targetSize = targetSize.rotatedForOrientation(image.imageOrientation) -#endif - let scale = cgImage.size.getScale(targetSize: targetSize, contentMode: .aspectFill) - let scaledSize = cgImage.size.scaled(by: scale) - let drawRect = scaledSize.centeredInRectWithSize(targetSize) - return image.draw(inCanvasWithSize: targetSize, drawRect: drawRect) - } - - func byDrawingInCircle(border: ImageProcessingOptions.Border?) -> PlatformImage? { - guard let squared = byCroppingToSquare(), let cgImage = squared.cgImage else { - return nil - } - let radius = CGFloat(cgImage.width) // Can use any dimension since image is a square - return squared.processed.byAddingRoundedCorners(radius: radius / 2.0, border: border) - } - - /// Draws an image in square by preserving an aspect ratio and filling the - /// square if needed. If the image is already a square, returns an original image. - func byCroppingToSquare() -> PlatformImage? { - guard let cgImage = image.cgImage else { - return nil - } - - guard cgImage.width != cgImage.height else { - return image // Already a square - } - - let imageSize = cgImage.size - let side = min(cgImage.width, cgImage.height) - let targetSize = CGSize(width: side, height: side) - let cropRect = CGRect(origin: .zero, size: targetSize).offsetBy( - dx: max(0, (imageSize.width - targetSize.width) / 2).rounded(.down), - dy: max(0, (imageSize.height - targetSize.height) / 2).rounded(.down) - ) - guard let cropped = cgImage.cropping(to: cropRect) else { - return nil - } - return PlatformImage.make(cgImage: cropped, source: image) - } - - /// Adds rounded corners with the given radius to the image. - /// - parameter radius: Radius in pixels. - /// - parameter border: Optional stroke border. - func byAddingRoundedCorners(radius: CGFloat, border: ImageProcessingOptions.Border? = nil) -> PlatformImage? { - guard let cgImage = image.cgImage else { - return nil - } - guard let ctx = CGContext.make(cgImage, size: cgImage.size, alphaInfo: .premultipliedLast) else { - return nil - } - let rect = CGRect(origin: CGPoint.zero, size: cgImage.size) - let path = CGPath(roundedRect: rect, cornerWidth: radius, cornerHeight: radius, transform: nil) - ctx.addPath(path) - ctx.clip() - ctx.draw(cgImage, in: CGRect(origin: CGPoint.zero, size: cgImage.size)) - - if let border { - ctx.setStrokeColor(border.color.cgColor) - ctx.addPath(path) - ctx.setLineWidth(border.width) - ctx.strokePath() - } - guard let outputCGImage = ctx.makeImage() else { - return nil - } - return PlatformImage.make(cgImage: outputCGImage, source: image) - } -} - -extension PlatformImage { - /// Draws the image in a `CGContext` in a canvas with the given size using - /// the specified draw rect. - /// - /// For example, if the canvas size is `CGSize(width: 10, height: 10)` and - /// the draw rect is `CGRect(x: -5, y: 0, width: 20, height: 10)` it would - /// draw the input image (which is horizontal based on the known draw rect) - /// in a square by centering it in the canvas. - /// - /// - parameter drawRect: `nil` by default. If `nil` will use the canvas rect. - func draw(inCanvasWithSize canvasSize: CGSize, drawRect: CGRect? = nil) -> PlatformImage? { - guard let cgImage else { - return nil - } - guard let ctx = CGContext.make(cgImage, size: canvasSize) else { - return nil - } - ctx.draw(cgImage, in: drawRect ?? CGRect(origin: .zero, size: canvasSize)) - guard let outputCGImage = ctx.makeImage() else { - return nil - } - return PlatformImage.make(cgImage: outputCGImage, source: self) - } - - /// Decompresses the input image by drawing in the the `CGContext`. - func decompressed(isUsingPrepareForDisplay: Bool) -> PlatformImage? { -#if os(iOS) || os(tvOS) || os(visionOS) - if isUsingPrepareForDisplay, #available(iOS 15.0, tvOS 15.0, *) { - return preparingForDisplay() - } -#endif - guard let cgImage else { - return nil - } - return draw(inCanvasWithSize: cgImage.size, drawRect: CGRect(origin: .zero, size: cgImage.size)) - } -} - -private extension CGContext { - static func make(_ image: CGImage, size: CGSize, alphaInfo: CGImageAlphaInfo? = nil) -> CGContext? { - let alphaInfo: CGImageAlphaInfo = alphaInfo ?? (image.isOpaque ? .noneSkipLast : .premultipliedLast) - - // Create the context which matches the input image. - if let ctx = CGContext( - data: nil, - width: Int(size.width), - height: Int(size.height), - bitsPerComponent: 8, - bytesPerRow: 0, - space: image.colorSpace ?? CGColorSpaceCreateDeviceRGB(), - bitmapInfo: alphaInfo.rawValue - ) { - return ctx - } - - // In case the combination of parameters (color space, bits per component, etc) - // is nit supported by Core Graphics, switch to default context. - // - Quartz 2D Programming Guide - // - https://github.com/kean/Nuke/issues/35 - // - https://github.com/kean/Nuke/issues/57 - return CGContext( - data: nil, - width: Int(size.width), height: Int(size.height), - bitsPerComponent: 8, - bytesPerRow: 0, - space: CGColorSpaceCreateDeviceRGB(), - bitmapInfo: alphaInfo.rawValue - ) - } -} - -extension CGFloat { - func converted(to unit: ImageProcessingOptions.Unit) -> CGFloat { - switch unit { - case .pixels: return self - case .points: return self * Screen.scale - } - } -} - -extension CGSize { - func getScale(targetSize: CGSize, contentMode: ImageProcessingOptions.ContentMode) -> CGFloat { - let scaleHor = targetSize.width / width - let scaleVert = targetSize.height / height - - switch contentMode { - case .aspectFill: - return max(scaleHor, scaleVert) - case .aspectFit: - return min(scaleHor, scaleVert) - } - } - - /// Calculates a rect such that the output rect will be in the center of - /// the rect of the input size (assuming origin: .zero) - func centeredInRectWithSize(_ targetSize: CGSize) -> CGRect { - // First, resize the original size to fill the target size. - CGRect(origin: .zero, size: self).offsetBy( - dx: -(width - targetSize.width) / 2, - dy: -(height - targetSize.height) / 2 - ) - } -} - -#if canImport(UIKit) -extension CGImagePropertyOrientation { - init(_ orientation: UIImage.Orientation) { - switch orientation { - case .up: self = .up - case .upMirrored: self = .upMirrored - case .down: self = .down - case .downMirrored: self = .downMirrored - case .left: self = .left - case .leftMirrored: self = .leftMirrored - case .right: self = .right - case .rightMirrored: self = .rightMirrored - @unknown default: self = .up - } - } -} -#endif - -#if canImport(UIKit) -private extension CGSize { - func rotatedForOrientation(_ imageOrientation: CGImagePropertyOrientation) -> CGSize { - switch imageOrientation { - case .left, .leftMirrored, .right, .rightMirrored: - return CGSize(width: height, height: width) // Rotate 90 degrees - case .up, .upMirrored, .down, .downMirrored: - return self - } - } - - func rotatedForOrientation(_ imageOrientation: UIImage.Orientation) -> CGSize { - switch imageOrientation { - case .left, .leftMirrored, .right, .rightMirrored: - return CGSize(width: height, height: width) // Rotate 90 degrees - case .up, .upMirrored, .down, .downMirrored: - return self - @unknown default: - return self - } - } -} -#endif - -#if os(macOS) -extension NSImage { - var cgImage: CGImage? { - cgImage(forProposedRect: nil, context: nil, hints: nil) - } - - var ciImage: CIImage? { - cgImage.map { CIImage(cgImage: $0) } - } - - static func make(cgImage: CGImage, source: NSImage) -> NSImage { - NSImage(cgImage: cgImage, size: .zero) - } - - convenience init(cgImage: CGImage) { - self.init(cgImage: cgImage, size: .zero) - } -} -#else -extension UIImage { - static func make(cgImage: CGImage, source: UIImage) -> UIImage { - UIImage(cgImage: cgImage, scale: source.scale, orientation: source.imageOrientation) - } -} -#endif - -extension CGImage { - /// Returns `true` if the image doesn't contain alpha channel. - var isOpaque: Bool { - let alpha = alphaInfo - return alpha == .none || alpha == .noneSkipFirst || alpha == .noneSkipLast - } - - var size: CGSize { - CGSize(width: width, height: height) - } -} - -extension CGSize { - func scaled(by scale: CGFloat) -> CGSize { - CGSize(width: width * scale, height: height * scale) - } - - func rounded() -> CGSize { - CGSize(width: CGFloat(round(width)), height: CGFloat(round(height))) - } -} - -@MainActor -enum Screen { -#if os(iOS) || os(tvOS) - /// Returns the current screen scale. - static let scale: CGFloat = UIScreen.main.scale -#elseif os(watchOS) - /// Returns the current screen scale. - static let scale: CGFloat = WKInterfaceDevice.current().screenScale -#else - /// Always returns 1. - static let scale: CGFloat = 1 -#endif -} - -#if os(macOS) -typealias Color = NSColor -#else -typealias Color = UIColor -#endif - -extension Color { - /// Returns a hex representation of the color, e.g. "#FFFFAA". - var hex: String { - var (r, g, b, a) = (CGFloat(0), CGFloat(0), CGFloat(0), CGFloat(0)) - getRed(&r, green: &g, blue: &b, alpha: &a) - let components = [r, g, b, a < 1 ? a : nil] - return "#" + components - .compactMap { $0 } - .map { String(format: "%02lX", lroundf(Float($0) * 255)) } - .joined() - } -} - -/// Creates an image thumbnail. Uses significantly less memory than other options. -func makeThumbnail(data: Data, options: ImageRequest.ThumbnailOptions) -> PlatformImage? { - guard let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceShouldCache: false] as CFDictionary) else { - return nil - } - - let maxPixelSize = getMaxPixelSize(for: source, options: options) - let options = [ - kCGImageSourceCreateThumbnailFromImageAlways: options.createThumbnailFromImageAlways, - kCGImageSourceCreateThumbnailFromImageIfAbsent: options.createThumbnailFromImageIfAbsent, - kCGImageSourceShouldCacheImmediately: options.shouldCacheImmediately, - kCGImageSourceCreateThumbnailWithTransform: options.createThumbnailWithTransform, - kCGImageSourceThumbnailMaxPixelSize: maxPixelSize] as [CFString: Any] - guard let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else { - return nil - } - return PlatformImage(cgImage: image) -} - -private func getMaxPixelSize(for source: CGImageSource, options thumbnailOptions: ImageRequest.ThumbnailOptions) -> CGFloat { - switch thumbnailOptions.targetSize { - case .fixed(let size): - return CGFloat(size) - case let .flexible(size, contentMode): - var targetSize = size.cgSize - let options = [kCGImageSourceShouldCache: false] as CFDictionary - guard let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, options) as? [CFString: Any], - let width = properties[kCGImagePropertyPixelWidth] as? CGFloat, - let height = properties[kCGImagePropertyPixelHeight] as? CGFloat else { - return max(targetSize.width, targetSize.height) - } - - let orientation = (properties[kCGImagePropertyOrientation] as? UInt32).flatMap(CGImagePropertyOrientation.init) ?? .up -#if canImport(UIKit) - targetSize = targetSize.rotatedForOrientation(orientation) -#endif - - let imageSize = CGSize(width: width, height: height) - let scale = imageSize.getScale(targetSize: targetSize, contentMode: contentMode) - let size = imageSize.scaled(by: scale).rounded() - return max(size.width, size.height) - } -} diff --git a/Nuke/Internal/ImagePublisher.swift b/Nuke/Internal/ImagePublisher.swift deleted file mode 100644 index 775e363..0000000 --- a/Nuke/Internal/ImagePublisher.swift +++ /dev/null @@ -1,86 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation -import Combine - -/// A publisher that starts a new `ImageTask` when a subscriber is added. -/// -/// If the requested image is available in the memory cache, the value is -/// delivered immediately. When the subscription is cancelled, the task also -/// gets cancelled. -/// -/// - note: In case the pipeline has `isProgressiveDecodingEnabled` option enabled -/// and the image being downloaded supports progressive decoding, the publisher -/// might emit more than a single value. -struct ImagePublisher: Publisher, Sendable { - typealias Output = ImageResponse - typealias Failure = ImagePipeline.Error - - let request: ImageRequest - let pipeline: ImagePipeline - - func receive(subscriber: S) where S: Subscriber, S: Sendable, Failure == S.Failure, Output == S.Input { - let subscription = ImageSubscription( - request: self.request, - pipeline: self.pipeline, - subscriber: subscriber - ) - subscriber.receive(subscription: subscription) - } -} - -private final class ImageSubscription: Subscription where S: Subscriber, S: Sendable, S.Input == ImageResponse, S.Failure == ImagePipeline.Error { - private var task: ImageTask? - private let subscriber: S? - private let request: ImageRequest - private let pipeline: ImagePipeline - private var isStarted = false - - init(request: ImageRequest, pipeline: ImagePipeline, subscriber: S) { - self.pipeline = pipeline - self.request = request - self.subscriber = subscriber - - } - - func request(_ demand: Subscribers.Demand) { - guard demand > 0 else { return } - guard let subscriber else { return } - - if let image = pipeline.cache[request] { - _ = subscriber.receive(ImageResponse(container: image, request: request, cacheType: .memory)) - - if !image.isPreview { - subscriber.receive(completion: .finished) - return - } - } - - task = pipeline.loadImage( - with: request, - queue: nil, - progress: { response, _, _ in - if let response { - // Send progressively decoded image (if enabled and if any) - _ = subscriber.receive(response) - } - }, - completion: { result in - switch result { - case let .success(response): - _ = subscriber.receive(response) - subscriber.receive(completion: .finished) - case let .failure(error): - subscriber.receive(completion: .failure(error)) - } - } - ) - } - - func cancel() { - task?.cancel() - task = nil - } -} diff --git a/Nuke/Internal/ImageRequestKeys.swift b/Nuke/Internal/ImageRequestKeys.swift deleted file mode 100644 index 85dd8dd..0000000 --- a/Nuke/Internal/ImageRequestKeys.swift +++ /dev/null @@ -1,124 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -extension ImageRequest { - - // MARK: - Cache Keys - - /// A key for processed image in memory cache. - func makeImageCacheKey() -> CacheKey { - CacheKey(self) - } - - /// A key for processed image data in disk cache. - func makeDataCacheKey() -> String { - "\(preferredImageId)\(thumbnail?.identifier ?? "")\(ImageProcessors.Composition(processors).identifier)" - } - - // MARK: - Load Keys - - /// A key for deduplicating operations for fetching the processed image. - func makeImageLoadKey() -> ImageLoadKey { - ImageLoadKey(self) - } - - /// A key for deduplicating operations for fetching the decoded image. - func makeDecodedImageLoadKey() -> DecodedImageLoadKey { - DecodedImageLoadKey(self) - } - - /// A key for deduplicating operations for fetching the original image. - func makeDataLoadKey() -> DataLoadKey { - DataLoadKey(self) - } -} - -/// Uniquely identifies a cache processed image. -final class CacheKey: Hashable, Sendable { - // Using a reference type turned out to be significantly faster - private let imageId: String? - private let thumbnail: ImageRequest.ThumbnailOptions? - private let processors: [any ImageProcessing] - - init(_ request: ImageRequest) { - self.imageId = request.preferredImageId - self.thumbnail = request.thumbnail - self.processors = request.processors - } - - func hash(into hasher: inout Hasher) { - hasher.combine(imageId) - hasher.combine(thumbnail) - hasher.combine(processors.count) - } - - static func == (lhs: CacheKey, rhs: CacheKey) -> Bool { - lhs.imageId == rhs.imageId && lhs.thumbnail == rhs.thumbnail && lhs.processors == rhs.processors - } -} - -/// Uniquely identifies a task of retrieving the processed image. -final class ImageLoadKey: Hashable, Sendable { - let cacheKey: CacheKey - let options: ImageRequest.Options - let loadKey: DataLoadKey - - init(_ request: ImageRequest) { - self.cacheKey = CacheKey(request) - self.options = request.options - self.loadKey = DataLoadKey(request) - } - - func hash(into hasher: inout Hasher) { - hasher.combine(cacheKey.hashValue) - hasher.combine(options.hashValue) - hasher.combine(loadKey.hashValue) - } - - static func == (lhs: ImageLoadKey, rhs: ImageLoadKey) -> Bool { - lhs.cacheKey == rhs.cacheKey && lhs.options == rhs.options && lhs.loadKey == rhs.loadKey - } -} - -/// Uniquely identifies a task of retrieving the decoded image. -struct DecodedImageLoadKey: Hashable { - let dataLoadKey: DataLoadKey - let thumbnail: ImageRequest.ThumbnailOptions? - - init(_ request: ImageRequest) { - self.dataLoadKey = DataLoadKey(request) - self.thumbnail = request.thumbnail - } -} - -/// Uniquely identifies a task of retrieving the original image dataa. -struct DataLoadKey: Hashable { - private let imageId: String? - private let cachePolicy: URLRequest.CachePolicy - private let allowsCellularAccess: Bool - - init(_ request: ImageRequest) { - self.imageId = request.imageId - switch request.resource { - case .url, .publisher: - self.cachePolicy = .useProtocolCachePolicy - self.allowsCellularAccess = true - case let .urlRequest(urlRequest): - self.cachePolicy = urlRequest.cachePolicy - self.allowsCellularAccess = urlRequest.allowsCellularAccess - } - } -} - -struct ImageProcessingKey: Equatable, Hashable { - let imageId: ObjectIdentifier - let processorId: AnyHashable - - init(image: ImageResponse, processor: any ImageProcessing) { - self.imageId = ObjectIdentifier(image.image) - self.processorId = processor.hashableIdentifier - } -} diff --git a/Nuke/Internal/LinkedList.swift b/Nuke/Internal/LinkedList.swift deleted file mode 100644 index 2f33ab6..0000000 --- a/Nuke/Internal/LinkedList.swift +++ /dev/null @@ -1,77 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// A doubly linked list. -final class LinkedList { - // first <-> node <-> ... <-> last - private(set) var first: Node? - private(set) var last: Node? - - deinit { - // This way we make sure that the deallocations do no happen recursively - // (and potentially overflow the stack). - removeAllElements() - } - - var isEmpty: Bool { - last == nil - } - - /// Adds an element to the end of the list. - @discardableResult - func append(_ element: Element) -> Node { - let node = Node(value: element) - append(node) - return node - } - - /// Adds a node to the end of the list. - func append(_ node: Node) { - if let last { - last.next = node - node.previous = last - self.last = node - } else { - last = node - first = node - } - } - - func remove(_ node: Node) { - node.next?.previous = node.previous // node.previous is nil if node=first - node.previous?.next = node.next // node.next is nil if node=last - if node === last { - last = node.previous - } - if node === first { - first = node.next - } - node.next = nil - node.previous = nil - } - - func removeAllElements() { - // avoid recursive Nodes deallocation - var node = first - while let next = node?.next { - node?.next = nil - next.previous = nil - node = next - } - last = nil - first = nil - } - - final class Node { - let value: Element - fileprivate var next: Node? - fileprivate var previous: Node? - - init(value: Element) { - self.value = value - } - } -} diff --git a/Nuke/Internal/Log.swift b/Nuke/Internal/Log.swift deleted file mode 100644 index 4c5bf97..0000000 --- a/Nuke/Internal/Log.swift +++ /dev/null @@ -1,53 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation -import os - -func signpost(_ object: AnyObject, _ name: StaticString, _ type: OSSignpostType) { - guard ImagePipeline.Configuration.isSignpostLoggingEnabled else { return } - - let signpostId = OSSignpostID(log: log, object: object) - os_signpost(type, log: log, name: name, signpostID: signpostId) -} - -func signpost(_ object: AnyObject, _ name: StaticString, _ type: OSSignpostType, _ message: @autoclosure () -> String) { - guard ImagePipeline.Configuration.isSignpostLoggingEnabled else { return } - - let signpostId = OSSignpostID(log: log, object: object) - os_signpost(type, log: log, name: name, signpostID: signpostId, "%{public}s", message()) -} - -func signpost(_ name: StaticString, _ work: () throws -> T) rethrows -> T { - try signpost(name, "", work) -} - -func signpost(_ name: StaticString, _ message: @autoclosure () -> String, _ work: () throws -> T) rethrows -> T { - guard ImagePipeline.Configuration.isSignpostLoggingEnabled else { return try work() } - - let signpostId = OSSignpostID(log: log) - let message = message() - if !message.isEmpty { - os_signpost(.begin, log: log, name: name, signpostID: signpostId, "%{public}s", message) - } else { - os_signpost(.begin, log: log, name: name, signpostID: signpostId) - } - let result = try work() - os_signpost(.end, log: log, name: name, signpostID: signpostId) - return result -} - -private let log = OSLog(subsystem: "com.github.kean.Nuke.ImagePipeline", category: "Image Loading") - -private let byteFormatter = ByteCountFormatter() - -enum Formatter { - static func bytes(_ count: Int) -> String { - bytes(Int64(count)) - } - - static func bytes(_ count: Int64) -> String { - byteFormatter.string(fromByteCount: count) - } -} diff --git a/Nuke/Internal/Operation.swift b/Nuke/Internal/Operation.swift deleted file mode 100644 index 31b9a74..0000000 --- a/Nuke/Internal/Operation.swift +++ /dev/null @@ -1,98 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -final class Operation: Foundation.Operation { - override var isExecuting: Bool { - get { - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - return _isExecuting - } - set { - os_unfair_lock_lock(lock) - _isExecuting = newValue - os_unfair_lock_unlock(lock) - - willChangeValue(forKey: "isExecuting") - didChangeValue(forKey: "isExecuting") - } - } - - override var isFinished: Bool { - get { - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - return _isFinished - } - set { - os_unfair_lock_lock(lock) - _isFinished = newValue - os_unfair_lock_unlock(lock) - - willChangeValue(forKey: "isFinished") - didChangeValue(forKey: "isFinished") - } - } - - typealias Starter = @Sendable (_ finish: @Sendable @escaping () -> Void) -> Void - private let starter: Starter - - private var _isExecuting = false - private var _isFinished = false - private var isFinishCalled = false - private let lock: os_unfair_lock_t - - deinit { - lock.deinitialize(count: 1) - lock.deallocate() - } - - init(starter: @escaping Starter) { - self.starter = starter - - self.lock = .allocate(capacity: 1) - self.lock.initialize(to: os_unfair_lock()) - } - - override func start() { - guard !isCancelled else { - isFinished = true - return - } - isExecuting = true - starter { [weak self] in - self?._finish() - } - } - - private func _finish() { - os_unfair_lock_lock(lock) - guard !isFinishCalled else { - return os_unfair_lock_unlock(lock) - } - isFinishCalled = true - os_unfair_lock_unlock(lock) - - isExecuting = false - isFinished = true - } -} - -extension OperationQueue { - /// Adds simple `BlockOperation`. - func add(_ closure: @Sendable @escaping () -> Void) -> BlockOperation { - let operation = BlockOperation(block: closure) - addOperation(operation) - return operation - } - - /// Adds asynchronous operation (`Nuke.Operation`) with the given starter. - func add(_ starter: @escaping Operation.Starter) -> Operation { - let operation = Operation(starter: starter) - addOperation(operation) - return operation - } -} diff --git a/Nuke/Internal/RateLimiter.swift b/Nuke/Internal/RateLimiter.swift deleted file mode 100644 index d85c34a..0000000 --- a/Nuke/Internal/RateLimiter.swift +++ /dev/null @@ -1,108 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// Controls the rate at which the work is executed. Uses the classic [token -/// bucket](https://en.wikipedia.org/wiki/Token_bucket) algorithm. -/// -/// The main use case for rate limiter is to support large (infinite) collections -/// of images by preventing trashing of underlying systems, primary URLSession. -/// -/// The implementation supports quick bursts of requests which can be executed -/// without any delays when "the bucket is full". This is important to prevent -/// rate limiter from affecting "normal" requests flow. -final class RateLimiter: @unchecked Sendable { - // This type isn't really Sendable and requires the caller to use the same - // queue as it does for synchronization. - - private let bucket: TokenBucket - private let queue: DispatchQueue - private var pending = LinkedList() // fast append, fast remove first - private var isExecutingPendingTasks = false - - typealias Work = () -> Bool - - /// Initializes the `RateLimiter` with the given configuration. - /// - parameters: - /// - queue: Queue on which to execute pending tasks. - /// - rate: Maximum number of requests per second. 80 by default. - /// - burst: Maximum number of requests which can be executed without any - /// delays when "bucket is full". 25 by default. - init(queue: DispatchQueue, rate: Int = 80, burst: Int = 25) { - self.queue = queue - self.bucket = TokenBucket(rate: Double(rate), burst: Double(burst)) - } - - /// - parameter closure: Returns `true` if the close was executed, `false` - /// if the work was cancelled. - func execute( _ work: @escaping Work) { - if !pending.isEmpty || !bucket.execute(work) { - pending.append(work) - setNeedsExecutePendingTasks() - } - } - - private func setNeedsExecutePendingTasks() { - guard !isExecutingPendingTasks else { - return - } - isExecutingPendingTasks = true - // Compute a delay such that by the time the closure is executed the - // bucket is refilled to a point that is able to execute at least one - // pending task. With a rate of 80 tasks we expect a refill every ~26 ms - // or as soon as the new tasks are added. - let bucketRate = 1000.0 / bucket.rate - let delay = Int(2.1 * bucketRate) // 14 ms for rate 80 (default) - let bounds = min(100, max(15, delay)) - queue.asyncAfter(deadline: .now() + .milliseconds(bounds)) { self.executePendingTasks() } - } - - private func executePendingTasks() { - while let node = pending.first, bucket.execute(node.value) { - pending.remove(node) - } - isExecutingPendingTasks = false - if !pending.isEmpty { // Not all pending items were executed - setNeedsExecutePendingTasks() - } - } -} - -private final class TokenBucket { - let rate: Double - private let burst: Double // maximum bucket size - private var bucket: Double - private var timestamp: TimeInterval // last refill timestamp - - /// - parameter rate: Rate (tokens/second) at which bucket is refilled. - /// - parameter burst: Bucket size (maximum number of tokens). - init(rate: Double, burst: Double) { - self.rate = rate - self.burst = burst - self.bucket = burst - self.timestamp = CFAbsoluteTimeGetCurrent() - } - - /// Returns `true` if the closure was executed, `false` if dropped. - func execute(_ work: () -> Bool) -> Bool { - refill() - guard bucket >= 1.0 else { - return false // bucket is empty - } - if work() { - bucket -= 1.0 // work was cancelled, no need to reduce the bucket - } - return true - } - - private func refill() { - let now = CFAbsoluteTimeGetCurrent() - bucket += rate * max(0, now - timestamp) // rate * (time delta) - timestamp = now - if bucket > burst { // prevent bucket overflow - bucket = burst - } - } -} diff --git a/Nuke/Internal/ResumableData.swift b/Nuke/Internal/ResumableData.swift deleted file mode 100644 index 97efc57..0000000 --- a/Nuke/Internal/ResumableData.swift +++ /dev/null @@ -1,134 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// Resumable data support. For more info see: -/// - https://developer.apple.com/library/content/qa/qa1761/_index.html -struct ResumableData: @unchecked Sendable { - let data: Data - let validator: String // Either Last-Modified or ETag - - init?(response: URLResponse, data: Data) { - // Check if "Accept-Ranges" is present and the response is valid. - guard !data.isEmpty, - let response = response as? HTTPURLResponse, - data.count < response.expectedContentLength, - response.statusCode == 200 /* OK */ || response.statusCode == 206, /* Partial Content */ - let acceptRanges = response.allHeaderFields["Accept-Ranges"] as? String, - acceptRanges.lowercased() == "bytes", - let validator = ResumableData._validator(from: response) else { - return nil - } - - // NOTE: https://developer.apple.com/documentation/foundation/httpurlresponse/1417930-allheaderfields - // HTTP headers are case insensitive. To simplify your code, certain - // header field names are canonicalized into their standard form. - // For example, if the server sends a content-length header, - // it is automatically adjusted to be Content-Length. - - self.data = data; self.validator = validator - } - - private static func _validator(from response: HTTPURLResponse) -> String? { - if let entityTag = response.allHeaderFields["ETag"] as? String { - return entityTag // Prefer ETag - } - // There seems to be a bug with ETag where HTTPURLResponse would canonicalize - // it to Etag instead of ETag - // https://bugs.swift.org/browse/SR-2429 - if let entityTag = response.allHeaderFields["Etag"] as? String { - return entityTag // Prefer ETag - } - if let lastModified = response.allHeaderFields["Last-Modified"] as? String { - return lastModified - } - return nil - } - - func resume(request: inout URLRequest) { - var headers = request.allHTTPHeaderFields ?? [:] - // "bytes=1000-" means bytes from 1000 up to the end (inclusive) - headers["Range"] = "bytes=\(data.count)-" - headers["If-Range"] = validator - request.allHTTPHeaderFields = headers - } - - // Check if the server decided to resume the response. - static func isResumedResponse(_ response: URLResponse) -> Bool { - // "206 Partial Content" (server accepted "If-Range") - (response as? HTTPURLResponse)?.statusCode == 206 - } -} - -/// Shared cache, uses the same memory pool across multiple pipelines. -final class ResumableDataStorage: @unchecked Sendable { - static let shared = ResumableDataStorage() - - private let lock = NSLock() - private var registeredPipelines = Set() - - private var cache: Cache? - - // MARK: Registration - - func register(_ pipeline: ImagePipeline) { - lock.lock() - defer { lock.unlock() } - - if registeredPipelines.isEmpty { - // 32 MB - cache = Cache(costLimit: 32000000, countLimit: 100) - } - registeredPipelines.insert(pipeline.id) - } - - func unregister(_ pipeline: ImagePipeline) { - lock.lock() - defer { lock.unlock() } - - registeredPipelines.remove(pipeline.id) - if registeredPipelines.isEmpty { - cache = nil // Deallocate storage - } - } - - func removeAllResponses() { - lock.lock() - defer { lock.unlock() } - - cache?.removeAllCachedValues() - } - - // MARK: Storage - - func removeResumableData(for request: ImageRequest, pipeline: ImagePipeline) -> ResumableData? { - lock.lock() - defer { lock.unlock() } - - guard let key = Key(request: request, pipeline: pipeline) else { return nil } - return cache?.removeValue(forKey: key) - } - - func storeResumableData(_ data: ResumableData, for request: ImageRequest, pipeline: ImagePipeline) { - lock.lock() - defer { lock.unlock() } - - guard let key = Key(request: request, pipeline: pipeline) else { return } - cache?.set(data, forKey: key, cost: data.data.count) - } - - private struct Key: Hashable { - let pipelineId: UUID - let imageId: String - - init?(request: ImageRequest, pipeline: ImagePipeline) { - guard let imageId = request.imageId else { - return nil - } - self.pipelineId = pipeline.id - self.imageId = imageId - } - } -} diff --git a/Nuke/Loading/DataLoader.swift b/Nuke/Loading/DataLoader.swift deleted file mode 100644 index 1af9c0c..0000000 --- a/Nuke/Loading/DataLoader.swift +++ /dev/null @@ -1,230 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// Provides basic networking using `URLSession`. -public final class DataLoader: DataLoading, @unchecked Sendable { - public let session: URLSession - private let impl = _DataLoader() - - /// Determines whether to deliver a partial response body in increments. By - /// default, `false`. - public var prefersIncrementalDelivery = false - - /// The delegate that gets called for the callbacks handled by the data loader. - /// You can use it for observing the session events and modifying some of the - /// task behavior, e.g. handling authentication challenges. - /// - /// For example, you can use it to log network requests using [Pulse](https://github.com/kean/Pulse) - /// which is optimized to work with images. - /// - /// ```swift - /// (ImagePipeline.shared.configuration.dataLoader as? DataLoader)?.delegate = URLSessionProxyDelegate() - /// ``` - /// - /// - note: The delegate is retained. - public var delegate: URLSessionDelegate? { - didSet { impl.delegate = delegate } - } - - deinit { - session.invalidateAndCancel() - } - - /// Initializes ``DataLoader`` with the given configuration. - /// - /// - parameters: - /// - configuration: `URLSessionConfiguration.default` with `URLCache` with - /// 0 MB memory capacity and 150 MB disk capacity by default. - /// - validate: Validates the response. By default, check if the status - /// code is in the acceptable range (`200..<300`). - public init(configuration: URLSessionConfiguration = DataLoader.defaultConfiguration, - validate: @escaping (URLResponse) -> Swift.Error? = DataLoader.validate) { - let queue = OperationQueue() - queue.maxConcurrentOperationCount = 1 - self.session = URLSession(configuration: configuration, delegate: impl, delegateQueue: queue) - self.session.sessionDescription = "Nuke URLSession" - self.impl.validate = validate - } - - /// Returns a default configuration which has a `sharedUrlCache` set - /// as a `urlCache`. - public static var defaultConfiguration: URLSessionConfiguration { - let conf = URLSessionConfiguration.default - conf.urlCache = DataLoader.sharedUrlCache - return conf - } - - /// Validates `HTTP` responses by checking that the status code is 2xx. If - /// it's not returns ``DataLoader/Error/statusCodeUnacceptable(_:)``. - public static func validate(response: URLResponse) -> Swift.Error? { - guard let response = response as? HTTPURLResponse else { - return nil - } - return (200..<300).contains(response.statusCode) ? nil : Error.statusCodeUnacceptable(response.statusCode) - } - -#if !os(macOS) && !targetEnvironment(macCatalyst) - private static let cachePath = "com.github.kean.Nuke.Cache" -#else - private static let cachePath: String = { - let cachePaths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true) - if let cachePath = cachePaths.first, let identifier = Bundle.main.bundleIdentifier { - return cachePath.appending("/" + identifier) - } - - return "" - }() -#endif - - /// Shared url cached used by a default ``DataLoader``. The cache is - /// initialized with 0 MB memory capacity and 150 MB disk capacity. - public static let sharedUrlCache: URLCache = { - let diskCapacity = 150 * 1048576 // 150 MB -#if targetEnvironment(macCatalyst) - return URLCache(memoryCapacity: 0, diskCapacity: diskCapacity, directory: URL(fileURLWithPath: cachePath)) -#else - return URLCache(memoryCapacity: 0, diskCapacity: diskCapacity, diskPath: cachePath) -#endif - }() - - public func loadData(with request: URLRequest, - didReceiveData: @escaping (Data, URLResponse) -> Void, - completion: @escaping (Swift.Error?) -> Void) -> any Cancellable { - let task = session.dataTask(with: request) - if #available(iOS 14.5, tvOS 14.5, watchOS 7.4, macOS 11.3, *) { - task.prefersIncrementalDelivery = prefersIncrementalDelivery - } - return impl.loadData(with: task, session: session, didReceiveData: didReceiveData, completion: completion) - } - - /// Errors produced by ``DataLoader``. - public enum Error: Swift.Error, CustomStringConvertible { - /// Validation failed. - case statusCodeUnacceptable(Int) - - public var description: String { - switch self { - case let .statusCodeUnacceptable(code): - return "Response status code was unacceptable: \(code.description)" - } - } - } -} - -// Actual data loader implementation. Hide NSObject inheritance, hide -// URLSessionDataDelegate conformance, and break retain cycle between URLSession -// and URLSessionDataDelegate. -private final class _DataLoader: NSObject, URLSessionDataDelegate { - var validate: (URLResponse) -> Swift.Error? = DataLoader.validate - private var handlers = [URLSessionTask: _Handler]() - var delegate: URLSessionDelegate? - - /// Loads data with the given request. - func loadData(with task: URLSessionDataTask, - session: URLSession, - didReceiveData: @escaping (Data, URLResponse) -> Void, - completion: @escaping (Error?) -> Void) -> any Cancellable { - let handler = _Handler(didReceiveData: didReceiveData, completion: completion) - session.delegateQueue.addOperation { // `URLSession` is configured to use this same queue - self.handlers[task] = handler - } - task.taskDescription = "Nuke Load Data" - task.resume() - return AnonymousCancellable { task.cancel() } - } - - // MARK: URLSessionDelegate - -#if !os(macOS) && !targetEnvironment(macCatalyst) && swift(>=5.7) - func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask) { - if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { - (delegate as? URLSessionTaskDelegate)?.urlSession?(session, didCreateTask: task) - } else { - // Doesn't exist on earlier versions - } - } -#endif - - func urlSession(_ session: URLSession, - dataTask: URLSessionDataTask, - didReceive response: URLResponse, - completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { - (delegate as? URLSessionDataDelegate)?.urlSession?(session, dataTask: dataTask, didReceive: response, completionHandler: { _ in }) - - guard let handler = handlers[dataTask] else { - completionHandler(.cancel) - return - } - if let error = validate(response) { - handler.completion(error) - completionHandler(.cancel) - return - } - completionHandler(.allow) - } - - func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - (delegate as? URLSessionTaskDelegate)?.urlSession?(session, task: task, didCompleteWithError: error) - assert(task is URLSessionDataTask) - guard let handler = handlers[task] else { - return - } - handlers[task] = nil - handler.completion(error) - } - - func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { - (delegate as? URLSessionTaskDelegate)?.urlSession?(session, task: task, didFinishCollecting: metrics) - } - - func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) { - (delegate as? URLSessionTaskDelegate)?.urlSession?(session, task: task, willPerformHTTPRedirection: response, newRequest: request, completionHandler: completionHandler) ?? - completionHandler(request) - } - - func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { - (delegate as? URLSessionTaskDelegate)?.urlSession?(session, taskIsWaitingForConnectivity: task) - } - - func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - (delegate as? URLSessionTaskDelegate)?.urlSession?(session, task: task, didReceive: challenge, completionHandler: completionHandler) ?? - completionHandler(.performDefaultHandling, nil) - } - - func urlSession(_ session: URLSession, task: URLSessionTask, willBeginDelayedRequest request: URLRequest, completionHandler: @escaping (URLSession.DelayedRequestDisposition, URLRequest?) -> Void) { - (delegate as? URLSessionTaskDelegate)?.urlSession?(session, task: task, willBeginDelayedRequest: request, completionHandler: completionHandler) ?? - completionHandler(.continueLoading, nil) - } - - // MARK: URLSessionDataDelegate - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - (delegate as? URLSessionDataDelegate)?.urlSession?(session, dataTask: dataTask, didReceive: data) - - guard let handler = handlers[dataTask], let response = dataTask.response else { - return - } - // Don't store data anywhere, just send it to the pipeline. - handler.didReceiveData(data, response) - } - - func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @Sendable @escaping (CachedURLResponse?) -> Void) { - (delegate as? URLSessionDataDelegate)?.urlSession?(session, dataTask: dataTask, willCacheResponse: proposedResponse, completionHandler: completionHandler) ?? - completionHandler(proposedResponse) - } - - // MARK: Internal - - private final class _Handler { - let didReceiveData: (Data, URLResponse) -> Void - let completion: (Error?) -> Void - - init(didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) { - self.didReceiveData = didReceiveData - self.completion = completion - } - } -} diff --git a/Nuke/Loading/DataLoading.swift b/Nuke/Loading/DataLoading.swift deleted file mode 100644 index e4da75e..0000000 --- a/Nuke/Loading/DataLoading.swift +++ /dev/null @@ -1,21 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// Fetches original image data. -public protocol DataLoading: Sendable { - /// - parameter didReceiveData: Can be called multiple times if streaming - /// is supported. - /// - parameter completion: Must be called once after all (or none in case - /// of an error) `didReceiveData` closures have been called. - func loadData(with request: URLRequest, - didReceiveData: @escaping (Data, URLResponse) -> Void, - completion: @escaping (Error?) -> Void) -> any Cancellable -} - -/// A unit of work that can be cancelled. -public protocol Cancellable: AnyObject, Sendable { - func cancel() -} diff --git a/Nuke/Pipeline/ImagePipeline.swift b/Nuke/Pipeline/ImagePipeline.swift deleted file mode 100644 index 34af0ca..0000000 --- a/Nuke/Pipeline/ImagePipeline.swift +++ /dev/null @@ -1,551 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation -import Combine - -#if canImport(UIKit) -import UIKit -#endif - -#if canImport(AppKit) -import AppKit -#endif - -/// The pipeline downloads and caches images, and prepares them for display. -public final class ImagePipeline: @unchecked Sendable { - /// Returns the shared image pipeline. - public static var shared: ImagePipeline { - get { _shared } - set { _shared = newValue } - } - - @Atomic - private static var _shared = ImagePipeline(configuration: .withURLCache) - - /// The pipeline configuration. - public let configuration: Configuration - - /// Provides access to the underlying caching subsystems. - public var cache: ImagePipeline.Cache { ImagePipeline.Cache(pipeline: self) } - - let delegate: any ImagePipelineDelegate - - private var tasks = [ImageTask: TaskSubscription]() - - private let tasksLoadData: TaskPool - private let tasksLoadImage: TaskPool - private let tasksFetchDecodedImage: TaskPool - private let tasksFetchOriginalImageData: TaskPool - private let tasksProcessImage: TaskPool - - // The queue on which the entire subsystem is synchronized. - let queue = DispatchQueue(label: "com.github.kean.Nuke.ImagePipeline", qos: .userInitiated) - private var isInvalidated = false - - private var nextTaskId: Int64 { - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - _nextTaskId += 1 - return _nextTaskId - } - private var _nextTaskId: Int64 = 0 - private let lock: os_unfair_lock_t - - let rateLimiter: RateLimiter? - let id = UUID() - - deinit { - lock.deinitialize(count: 1) - lock.deallocate() - - ResumableDataStorage.shared.unregister(self) - } - - /// Initializes the instance with the given configuration. - /// - /// - parameters: - /// - configuration: The pipeline configuration. - /// - delegate: Provides more ways to customize the pipeline behavior on per-request basis. - public init(configuration: Configuration = Configuration(), delegate: (any ImagePipelineDelegate)? = nil) { - self.configuration = configuration - self.rateLimiter = configuration.isRateLimiterEnabled ? RateLimiter(queue: queue) : nil - self.delegate = delegate ?? ImagePipelineDefaultDelegate() - (configuration.dataLoader as? DataLoader)?.prefersIncrementalDelivery = configuration.isProgressiveDecodingEnabled - - let isCoalescingEnabled = configuration.isTaskCoalescingEnabled - self.tasksLoadData = TaskPool(isCoalescingEnabled) - self.tasksLoadImage = TaskPool(isCoalescingEnabled) - self.tasksFetchDecodedImage = TaskPool(isCoalescingEnabled) - self.tasksFetchOriginalImageData = TaskPool(isCoalescingEnabled) - self.tasksProcessImage = TaskPool(isCoalescingEnabled) - - self.lock = .allocate(capacity: 1) - self.lock.initialize(to: os_unfair_lock()) - - ResumableDataStorage.shared.register(self) - } - - /// A convenience way to initialize the pipeline with a closure. - /// - /// Example usage: - /// - /// ```swift - /// ImagePipeline { - /// $0.dataCache = try? DataCache(name: "com.myapp.datacache") - /// $0.dataCachePolicy = .automatic - /// } - /// ``` - /// - /// - parameters: - /// - configuration: The pipeline configuration. - /// - delegate: Provides more ways to customize the pipeline behavior on per-request basis. - public convenience init(delegate: (any ImagePipelineDelegate)? = nil, _ configure: (inout ImagePipeline.Configuration) -> Void) { - var configuration = ImagePipeline.Configuration() - configure(&configuration) - self.init(configuration: configuration, delegate: delegate) - } - - /// Invalidates the pipeline and cancels all outstanding tasks. Any new - /// requests will immediately fail with ``ImagePipeline/Error/pipelineInvalidated`` error. - public func invalidate() { - queue.async { - guard !self.isInvalidated else { return } - self.isInvalidated = true - self.tasks.keys.forEach { self.cancel($0) } - } - } - - // MARK: - Loading Images (Async/Await) - - /// Creates a task with the given URL. - public func imageTask(with url: URL) -> AsyncImageTask { - imageTask(with: ImageRequest(url: url)) - } - - /// Creates a task with the given request. - public func imageTask(with request: ImageRequest) -> AsyncImageTask { - let imageTask = makeImageTask(request: request, queue: queue) - delegate.imageTaskCreated(imageTask, pipeline: self) - let context = AsyncTaskContext() - let task = Task { - try await self.image(for: imageTask, context: context) - } - let progress = AsyncStream { context.progress = $0 } - let previews = AsyncStream { context.previews = $0 } - return AsyncImageTask(imageTask: imageTask, task: task, progress: progress, previews: previews) - } - - /// Returns an image for the given URL. - /// - /// - parameters: - /// - request: An image URL. - public func image(for url: URL) async throws -> PlatformImage { - try await image(for: ImageRequest(url: url)) - } - - /// Returns an image for the given request. - /// - /// - parameters: - /// - request: An image request. - public func image(for request: ImageRequest) async throws -> PlatformImage { - // Optimization: fetch image directly without creating an associated task - let task = makeImageTask(request: request, queue: queue) - delegate.imageTaskCreated(task, pipeline: self) - return try await image(for: task).image - } - - private func image(for task: ImageTask, context: AsyncTaskContext? = nil) async throws -> ImageResponse { - try await withTaskCancellationHandler(operation: { - try await withUnsafeThrowingContinuation { continuation in - self.queue.async { - guard task.state != .cancelled else { - return continuation.resume(throwing: CancellationError()) - } - task.onCancel = { - context?.progress?.finish() - context?.previews?.finish() - continuation.resume(throwing: CancellationError()) - } - self.startImageTask(task, progress: { response, progress in - if let response { - context?.previews?.yield(response) - } else { - context?.progress?.yield(progress) - } - }, completion: { result in - context?.progress?.finish() - context?.previews?.finish() - continuation.resume(with: result) - }) - } - } - }, onCancel: { - task.cancel() - }) - } - - // MARK: - Loading Data (Async/Await) - - /// Returns image data for the given URL. - /// - /// - parameter request: An image request. - @discardableResult - public func data(for url: URL) async throws -> (Data, URLResponse?) { - try await data(for: ImageRequest(url: url)) - } - - /// Returns image data for the given request. - /// - /// - parameter request: An image request. - @discardableResult - public func data(for request: ImageRequest) async throws -> (Data, URLResponse?) { - let task = makeImageTask(request: request, queue: nil, isDataTask: true) - return try await withTaskCancellationHandler(operation: { - try await withUnsafeThrowingContinuation { continuation in - self.queue.async { - guard task.state != .cancelled else { - return continuation.resume(throwing: CancellationError()) - } - task.onCancel = { - continuation.resume(throwing: CancellationError()) - } - self.startDataTask(task, progress: nil) { result in - continuation.resume(with: result.map { $0 }) - } - } - } - }, onCancel: { - task.cancel() - }) - } - - // MARK: - Loading Images (Closures) - - /// Loads an image for the given request. - /// - /// - parameters: - /// - request: An image request. - /// - completion: A closure to be called on the main thread when the request - /// is finished. - @discardableResult public func loadImage( - with url: URL, - completion: @escaping (_ result: Result) -> Void - ) -> ImageTask { - loadImage(with: ImageRequest(url: url), queue: nil, progress: nil, completion: completion) - } - - /// Loads an image for the given request. - /// - /// - parameters: - /// - request: An image request. - /// - completion: A closure to be called on the main thread when the request - /// is finished. - @discardableResult public func loadImage( - with request: ImageRequest, - completion: @escaping (_ result: Result) -> Void - ) -> ImageTask { - loadImage(with: request, queue: nil, progress: nil, completion: completion) - } - - /// Loads an image for the given request. - /// - /// - parameters: - /// - request: An image request. - /// - queue: A queue on which to execute `progress` and `completion` callbacks. - /// By default, the pipeline uses `.main` queue. - /// - progress: A closure to be called periodically on the main thread when - /// the progress is updated. - /// - completion: A closure to be called on the main thread when the request - /// is finished. - @discardableResult public func loadImage( - with request: ImageRequest, - queue: DispatchQueue? = nil, - progress: ((_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)?, - completion: @escaping (_ result: Result) -> Void - ) -> ImageTask { - loadImage(with: request, isConfined: false, queue: queue, progress: { - progress?($0, $1.completed, $1.total) - }, completion: completion) - } - - func loadImage( - with request: ImageRequest, - isConfined: Bool, - queue callbackQueue: DispatchQueue?, - progress: ((ImageResponse?, ImageTask.Progress) -> Void)?, - completion: @escaping (Result) -> Void - ) -> ImageTask { - let task = makeImageTask(request: request, queue: callbackQueue) - delegate.imageTaskCreated(task, pipeline: self) - @Sendable func start() { - startImageTask(task, progress: progress, completion: completion) - } - if isConfined { - start() - } else { - self.queue.async { start() } - } - return task - } - - private func startImageTask( - _ task: ImageTask, - progress progressHandler: ((ImageResponse?, ImageTask.Progress) -> Void)?, - completion: @escaping (Result) -> Void - ) { - guard !isInvalidated else { - dispatchCallback(to: task.callbackQueue) { - let error = Error.pipelineInvalidated - self.delegate.imageTask(task, didCompleteWithResult: .failure(error), pipeline: self) - completion(.failure(error)) - } - return - } - - delegate.imageTaskDidStart(task, pipeline: self) - - tasks[task] = makeTaskLoadImage(for: task.request) - .subscribe(priority: task.priority.taskPriority, subscriber: task) { [weak self, weak task] event in - guard let self, let task else { return } - - if event.isCompleted { - self.tasks[task] = nil - } - - self.dispatchCallback(to: task.callbackQueue) { - guard task.state != .cancelled else { return } - if event.isCompleted { - task.didComplete() // Important: called on callback queue and in this order - } - switch event { - case let .value(response, isCompleted): - if isCompleted { - self.delegate.imageTask(task, didCompleteWithResult: .success(response), pipeline: self) - completion(.success(response)) - } else { - self.delegate.imageTask(task, didReceivePreview: response, pipeline: self) - progressHandler?(response, task.progress) - } - case let .progress(progress): - self.delegate.imageTask(task, didUpdateProgress: progress, pipeline: self) - task.progress = progress - progressHandler?(nil, progress) - case let .error(error): - self.delegate.imageTask(task, didCompleteWithResult: .failure(error), pipeline: self) - completion(.failure(error)) - } - } - } - } - - private func makeImageTask(request: ImageRequest, queue: DispatchQueue?, isDataTask: Bool = false) -> ImageTask { - let task = ImageTask(taskId: nextTaskId, request: request) - task.pipeline = self - task.callbackQueue = queue - task.isDataTask = isDataTask - return task - } - - // MARK: - Loading Data (Closures) - - /// Loads image data for the given request. The data doesn't get decoded - /// or processed in any other way. - @discardableResult public func loadData( - with url: URL, - completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void - ) -> ImageTask { - loadData(with: ImageRequest(url: url), queue: nil, progress: nil, completion: completion) - } - - /// Loads image data for the given request. The data doesn't get decoded - /// or processed in any other way. - @discardableResult public func loadData( - with request: ImageRequest, - completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void - ) -> ImageTask { - loadData(with: request, queue: nil, progress: nil, completion: completion) - } - - /// Loads the image data for the given request. The data doesn't get decoded - /// or processed in any other way. - /// - /// You can call ``loadImage(with:completion:)-43osv`` for the request at any point after calling - /// ``loadData(with:completion:)-6cwk3``, the pipeline will use the same operation to load the data, - /// no duplicated work will be performed. - /// - /// - parameters: - /// - request: An image request. - /// - queue: A queue on which to execute `progress` and `completion` - /// callbacks. By default, the pipeline uses `.main` queue. - /// - progress: A closure to be called periodically on the main thread when the progress is updated. - /// - completion: A closure to be called on the main thread when the request is finished. - @discardableResult public func loadData( - with request: ImageRequest, - queue: DispatchQueue? = nil, - progress: ((_ completed: Int64, _ total: Int64) -> Void)?, - completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void - ) -> ImageTask { - loadData(with: request, isConfined: false, queue: queue, progress: progress, completion: completion) - } - - func loadData( - with request: ImageRequest, - isConfined: Bool, - queue: DispatchQueue?, - progress: ((_ completed: Int64, _ total: Int64) -> Void)?, - completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void - ) -> ImageTask { - let task = makeImageTask(request: request, queue: queue, isDataTask: true) - @Sendable func start() { - startDataTask(task, progress: progress, completion: completion) - } - if isConfined { - start() - } else { - self.queue.async { start() } - } - return task - } - - private func startDataTask( - _ task: ImageTask, - progress progressHandler: ((_ completed: Int64, _ total: Int64) -> Void)?, - completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void - ) { - guard !isInvalidated else { - dispatchCallback(to: task.callbackQueue) { - let error = Error.pipelineInvalidated - self.delegate.imageTask(task, didCompleteWithResult: .failure(error), pipeline: self) - completion(.failure(error)) - } - return - } - - tasks[task] = makeTaskLoadData(for: task.request) - .subscribe(priority: task.priority.taskPriority, subscriber: task) { [weak self, weak task] event in - guard let self, let task else { return } - - if event.isCompleted { - self.tasks[task] = nil - } - - self.dispatchCallback(to: task.callbackQueue) { - guard task.state != .cancelled else { return } - if event.isCompleted { - task.didComplete() // Important: called on callback queue and in this order - } - switch event { - case let .value(response, isCompleted): - if isCompleted { - completion(.success(response)) - } - case let .progress(progress): - task.progress = progress - progressHandler?(progress.completed, progress.total) - case let .error(error): - completion(.failure(error)) - } - } - } - } - - // MARK: - Loading Images (Combine) - - /// Returns a publisher which starts a new ``ImageTask`` when a subscriber is added. - public func imagePublisher(with url: URL) -> AnyPublisher { - imagePublisher(with: ImageRequest(url: url)) - } - - /// Returns a publisher which starts a new ``ImageTask`` when a subscriber is added. - public func imagePublisher(with request: ImageRequest) -> AnyPublisher { - ImagePublisher(request: request, pipeline: self).eraseToAnyPublisher() - } - - // MARK: - Image Task Events - - func imageTaskCancelCalled(_ task: ImageTask) { - queue.async { - self.cancel(task) - } - } - - private func cancel(_ task: ImageTask) { - guard let subscription = tasks.removeValue(forKey: task) else { return } - dispatchCallback(to: task.callbackQueue) { - if !task.isDataTask { - self.delegate.imageTaskDidCancel(task, pipeline: self) - } - task.onCancel?() // Order is important - } - subscription.unsubscribe() - } - - func imageTaskUpdatePriorityCalled(_ task: ImageTask, priority: ImageRequest.Priority) { - queue.async { - self.tasks[task]?.setPriority(priority.taskPriority) - } - } - - private func dispatchCallback(to callbackQueue: DispatchQueue?, _ closure: @escaping () -> Void) { - if callbackQueue === self.queue { - closure() - } else { - (callbackQueue ?? self.configuration.callbackQueue).async(execute: closure) - } - } - - // MARK: - Task Factory (Private) - - // When you request an image or image data, the pipeline creates a graph of tasks - // (some tasks are added to the graph on demand). - // - // `loadImage()` call is represented by TaskLoadImage: - // - // TaskLoadImage -> TaskFetchDecodedImage -> TaskFetchOriginalImageData - // -> TaskProcessImage - // - // `loadData()` call is represented by TaskLoadData: - // - // TaskLoadData -> TaskFetchOriginalImageData - // - // - // Each task represents a resource or a piece of work required to produce the - // final result. The pipeline reduces the amount of duplicated work by coalescing - // the tasks that represent the same work. For example, if you all `loadImage()` - // and `loadData()` with the same request, only on `TaskFetchOriginalImageData` - // is created. The work is split between tasks to minimize any duplicated work. - - func makeTaskLoadImage(for request: ImageRequest) -> AsyncTask.Publisher { - tasksLoadImage.publisherForKey(request.makeImageLoadKey()) { - TaskLoadImage(self, request) - } - } - - func makeTaskLoadData(for request: ImageRequest) -> AsyncTask<(Data, URLResponse?), Error>.Publisher { - tasksLoadData.publisherForKey(request.makeImageLoadKey()) { - TaskLoadData(self, request) - } - } - - func makeTaskProcessImage(key: ImageProcessingKey, process: @escaping () throws -> ImageResponse) -> AsyncTask.Publisher { - tasksProcessImage.publisherForKey(key) { - OperationTask(self, configuration.imageProcessingQueue, process) - } - } - - func makeTaskFetchDecodedImage(for request: ImageRequest) -> AsyncTask.Publisher { - tasksFetchDecodedImage.publisherForKey(request.makeDecodedImageLoadKey()) { - TaskFetchDecodedImage(self, request) - } - } - - func makeTaskFetchOriginalImageData(for request: ImageRequest) -> AsyncTask<(Data, URLResponse?), Error>.Publisher { - tasksFetchOriginalImageData.publisherForKey(request.makeDataLoadKey()) { - request.publisher == nil ? - TaskFetchOriginalImageData(self, request) : - TaskFetchWithPublisher(self, request) - } - } -} diff --git a/Nuke/Pipeline/ImagePipelineCache.swift b/Nuke/Pipeline/ImagePipelineCache.swift deleted file mode 100644 index 895c4fb..0000000 --- a/Nuke/Pipeline/ImagePipelineCache.swift +++ /dev/null @@ -1,261 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -extension ImagePipeline { - /// Provides a set of convenience APIs for managing the pipeline cache layers, - /// including ``ImageCaching`` (memory cache) and ``DataCaching`` (disk cache). - /// - /// - important: This class doesn't work with a `URLCache`. For more info, - /// see . - public struct Cache: Sendable { - let pipeline: ImagePipeline - private var configuration: ImagePipeline.Configuration { pipeline.configuration } - } -} - -extension ImagePipeline.Cache { - // MARK: Subscript (Memory Cache) - - /// Returns an image from the memory cache for the given URL. - public subscript(url: URL) -> ImageContainer? { - get { self[ImageRequest(url: url)] } - nonmutating set { self[ImageRequest(url: url)] = newValue } - } - - /// Returns an image from the memory cache for the given request. - public subscript(request: ImageRequest) -> ImageContainer? { - get { - cachedImageFromMemoryCache(for: request) - } - nonmutating set { - if let image = newValue { - storeCachedImageInMemoryCache(image, for: request) - } else { - removeCachedImageFromMemoryCache(for: request) - } - } - } - - // MARK: Cached Images - - /// Returns a cached image any of the caches. - /// - /// - note: Respects request options such as its cache policy. - /// - /// - parameters: - /// - request: The request. Make sure to remove the processors if you want - /// to retrieve an original image (if it's stored). - /// - caches: `[.all]`, by default. - public func cachedImage(for request: ImageRequest, caches: Caches = [.all]) -> ImageContainer? { - if caches.contains(.memory) { - if let image = cachedImageFromMemoryCache(for: request) { - return image - } - } - if caches.contains(.disk) { - if let data = cachedData(for: request), - let image = decodeImageData(data, for: request) { - return image - } - } - return nil - } - - /// Stores the image in all caches. To store image in the disk cache, it - /// will be encoded (see ``ImageEncoding``) - /// - /// - note: Respects request cache options. - /// - /// - note: Default ``DataCache`` stores data asynchronously, so it's safe - /// to call this method even from the main thread. - /// - /// - note: Image previews are not stored. - /// - /// - parameters: - /// - request: The request. Make sure to remove the processors if you want - /// to retrieve an original image (if it's stored). - /// - caches: `[.all]`, by default. - public func storeCachedImage(_ image: ImageContainer, for request: ImageRequest, caches: Caches = [.all]) { - if caches.contains(.memory) { - storeCachedImageInMemoryCache(image, for: request) - } - if caches.contains(.disk) { - if let data = encodeImage(image, for: request) { - storeCachedData(data, for: request) - } - } - } - - /// Removes the image from all caches. - public func removeCachedImage(for request: ImageRequest, caches: Caches = [.all]) { - if caches.contains(.memory) { - removeCachedImageFromMemoryCache(for: request) - } - if caches.contains(.disk) { - removeCachedData(for: request) - } - } - - /// Returns `true` if any of the caches contain the image. - public func containsCachedImage(for request: ImageRequest, caches: Caches = [.all]) -> Bool { - if caches.contains(.memory) && cachedImageFromMemoryCache(for: request) != nil { - return true - } - if caches.contains(.disk), let dataCache = dataCache(for: request) { - let key = makeDataCacheKey(for: request) - return dataCache.containsData(for: key) - } - return false - } - - private func cachedImageFromMemoryCache(for request: ImageRequest) -> ImageContainer? { - guard !request.options.contains(.disableMemoryCacheReads) else { - return nil - } - guard let imageCache = imageCache(for: request) else { - return nil - } - return imageCache[makeImageCacheKey(for: request)] - } - - private func storeCachedImageInMemoryCache(_ image: ImageContainer, for request: ImageRequest) { - guard !request.options.contains(.disableMemoryCacheWrites) else { - return - } - guard !image.isPreview || configuration.isStoringPreviewsInMemoryCache else { - return - } - guard let imageCache = imageCache(for: request) else { - return - } - imageCache[makeImageCacheKey(for: request)] = image - } - - private func removeCachedImageFromMemoryCache(for request: ImageRequest) { - guard let imageCache = imageCache(for: request) else { - return - } - imageCache[makeImageCacheKey(for: request)] = nil - } - - // MARK: Cached Data - - /// Returns cached data for the given request. - public func cachedData(for request: ImageRequest) -> Data? { - guard !request.options.contains(.disableDiskCacheReads) else { - return nil - } - guard let dataCache = dataCache(for: request) else { - return nil - } - let key = makeDataCacheKey(for: request) - return dataCache.cachedData(for: key) - } - - /// Stores data for the given request. - /// - /// - note: Default ``DataCache`` stores data asynchronously, so it's safe - /// to call this method even from the main thread. - public func storeCachedData(_ data: Data, for request: ImageRequest) { - guard let dataCache = dataCache(for: request), - !request.options.contains(.disableDiskCacheWrites) else { - return - } - let key = makeDataCacheKey(for: request) - dataCache.storeData(data, for: key) - } - - /// Returns true if the data cache contains data for the given image - public func containsData(for request: ImageRequest) -> Bool { - guard let dataCache = dataCache(for: request) else { - return false - } - return dataCache.containsData(for: makeDataCacheKey(for: request)) - } - - /// Removes cached data for the given request. - public func removeCachedData(for request: ImageRequest) { - guard let dataCache = dataCache(for: request) else { - return - } - let key = makeDataCacheKey(for: request) - dataCache.removeData(for: key) - } - - // MARK: Keys - - /// Returns image cache (memory cache) key for the given request. - public func makeImageCacheKey(for request: ImageRequest) -> ImageCacheKey { - if let customKey = pipeline.delegate.cacheKey(for: request, pipeline: pipeline) { - return ImageCacheKey(key: customKey) - } - return ImageCacheKey(request: request) // Use the default key - } - - /// Returns data cache (disk cache) key for the given request. - public func makeDataCacheKey(for request: ImageRequest) -> String { - if let customKey = pipeline.delegate.cacheKey(for: request, pipeline: pipeline) { - return customKey - } - return request.makeDataCacheKey() // Use the default key - } - - // MARK: Misc - - /// Removes both images and data from all cache layes. - /// - /// - important: It clears only caches set in the pipeline configuration. If - /// you implement ``ImagePipelineDelegate`` that uses different caches for - /// different requests, this won't remove images from them. - public func removeAll(caches: Caches = [.all]) { - if caches.contains(.memory) { - configuration.imageCache?.removeAll() - } - if caches.contains(.disk) { - configuration.dataCache?.removeAll() - } - } - - // MARK: Private - - private func decodeImageData(_ data: Data, for request: ImageRequest) -> ImageContainer? { - let context = ImageDecodingContext(request: request, data: data, isCompleted: true, urlResponse: nil, cacheType: .disk) - guard let decoder = pipeline.delegate.imageDecoder(for: context, pipeline: pipeline) else { - return nil - } - return (try? decoder.decode(context))?.container - } - - private func encodeImage(_ image: ImageContainer, for request: ImageRequest) -> Data? { - let context = ImageEncodingContext(request: request, image: image.image, urlResponse: nil) - let encoder = pipeline.delegate.imageEncoder(for: context, pipeline: pipeline) - return encoder.encode(image, context: context) - } - - private func imageCache(for request: ImageRequest) -> (any ImageCaching)? { - pipeline.delegate.imageCache(for: request, pipeline: pipeline) - } - - private func dataCache(for request: ImageRequest) -> (any DataCaching)? { - pipeline.delegate.dataCache(for: request, pipeline: pipeline) - } - - // MARK: Options - - /// Describes a set of cache layers to use. - public struct Caches: OptionSet { - public let rawValue: Int - public init(rawValue: Int) { - self.rawValue = rawValue - } - - public static let memory = Caches(rawValue: 1 << 0) - public static let disk = Caches(rawValue: 1 << 1) - public static let all: Caches = [.memory, .disk] - } -} - -extension ImagePipeline.Cache.Caches: Sendable {} diff --git a/Nuke/Pipeline/ImagePipelineConfiguration.swift b/Nuke/Pipeline/ImagePipelineConfiguration.swift deleted file mode 100644 index 59e1dcd..0000000 --- a/Nuke/Pipeline/ImagePipelineConfiguration.swift +++ /dev/null @@ -1,250 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -extension ImagePipeline { - /// The pipeline configuration. - public struct Configuration: @unchecked Sendable { - // MARK: - Dependencies - - /// Data loader used by the pipeline. - public var dataLoader: any DataLoading - - /// Data cache used by the pipeline. - public var dataCache: (any DataCaching)? - - /// Image cache used by the pipeline. - public var imageCache: (any ImageCaching)? { - // This exists simply to ensure we don't init ImageCache.shared if the - // user provides their own instance. - get { isCustomImageCacheProvided ? customImageCache : ImageCache.shared } - set { - customImageCache = newValue - isCustomImageCacheProvided = true - } - } - private var customImageCache: (any ImageCaching)? - - /// Default implementation uses shared ``ImageDecoderRegistry`` to create - /// a decoder that matches the context. - public var makeImageDecoder: @Sendable (ImageDecodingContext) -> (any ImageDecoding)? = { - ImageDecoderRegistry.shared.decoder(for: $0) - } - - /// Returns `ImageEncoders.Default()` by default. - public var makeImageEncoder: @Sendable (ImageEncodingContext) -> any ImageEncoding = { _ in - ImageEncoders.Default() - } - - // MARK: - Options - - /// Decompresses the loaded images. By default, enabled on all platforms - /// except for `macOS`. - /// - /// Decompressing compressed image formats (such as JPEG) can significantly - /// improve drawing performance as it allows a bitmap representation to be - /// created in a background rather than on the main thread. - public var isDecompressionEnabled: Bool { - get { _isDecompressionEnabled } - set { _isDecompressionEnabled = newValue } - } - - /// Set this to `true` to use native `preparingForDisplay()` method for - /// decompression on iOS and tvOS 15.0 and later. Disabled by default. - /// If disabled, CoreGraphics-based decompression is used. - public var isUsingPrepareForDisplay: Bool = false - -#if os(macOS) - var _isDecompressionEnabled = false -#else - var _isDecompressionEnabled = true -#endif - - /// If you use an aggressive disk cache ``DataCaching``, you can specify - /// a cache policy with multiple available options and - /// ``ImagePipeline/DataCachePolicy/storeOriginalData`` used by default. - public var dataCachePolicy = ImagePipeline.DataCachePolicy.storeOriginalData - - /// `true` by default. If `true` the pipeline avoids duplicated work when - /// loading images. The work only gets cancelled when all the registered - /// requests are. The pipeline also automatically manages the priority of the - /// deduplicated work. - /// - /// Let's take these two requests for example: - /// - /// ```swift - /// let url = URL(string: "http://example.com/image") - /// pipeline.loadImage(with: ImageRequest(url: url, processors: [ - /// .resize(size: CGSize(width: 44, height: 44)), - /// .gaussianBlur(radius: 8) - /// ])) - /// pipeline.loadImage(with: ImageRequest(url: url, processors: [ - /// .resize(size: CGSize(width: 44, height: 44)) - /// ])) - /// ``` - /// - /// Nuke will load the image data only once, resize the image once and - /// apply the blur also only once. There is no duplicated work done at - /// any stage. - public var isTaskCoalescingEnabled = true - - /// `true` by default. If `true` the pipeline will rate limit requests - /// to prevent trashing of the underlying systems (e.g. `URLSession`). - /// The rate limiter only comes into play when the requests are started - /// and cancelled at a high rate (e.g. scrolling through a collection view). - public var isRateLimiterEnabled = true - - /// `false` by default. If `true` the pipeline will try to produce a new - /// image each time it receives a new portion of data from data loader. - /// The decoder used by the image loading session determines whether - /// to produce a partial image or not. The default image decoder - /// ``ImageDecoders/Default`` supports progressive JPEG decoding. - public var isProgressiveDecodingEnabled = false - - /// `true` by default. If `true`, the pipeline will store all of the - /// progressively generated previews in the memory cache. All of the - /// previews have ``ImageContainer/isPreview`` flag set to `true`. - public var isStoringPreviewsInMemoryCache = true - - /// If the data task is terminated (either because of a failure or a - /// cancellation) and the image was partially loaded, the next load will - /// resume where it left off. Supports both validators (`ETag`, - /// `Last-Modified`). Resumable downloads are enabled by default. - public var isResumableDataEnabled = true - - /// A queue on which all callbacks, like `progress` and `completion` - /// callbacks are called. `.main` by default. - public var callbackQueue = DispatchQueue.main - - // MARK: - Options (Shared) - - /// `false` by default. If `true`, enables `os_signpost` logging for - /// measuring performance. You can visually see all the performance - /// metrics in `os_signpost` Instrument. For more information see - /// https://developer.apple.com/documentation/os/logging and - /// https://developer.apple.com/videos/play/wwdc2018/405/. - public static var isSignpostLoggingEnabled = false - - private var isCustomImageCacheProvided = false - - var debugIsSyncImageEncoding = false - - // MARK: - Operation Queues - - /// Data loading queue. Default maximum concurrent task count is 6. - public var dataLoadingQueue = OperationQueue(maxConcurrentCount: 6) - - /// Data caching queue. Default maximum concurrent task count is 2. - public var dataCachingQueue = OperationQueue(maxConcurrentCount: 2) - - /// Image decoding queue. Default maximum concurrent task count is 1. - public var imageDecodingQueue = OperationQueue(maxConcurrentCount: 1) - - /// Image encoding queue. Default maximum concurrent task count is 1. - public var imageEncodingQueue = OperationQueue(maxConcurrentCount: 1) - - /// Image processing queue. Default maximum concurrent task count is 2. - public var imageProcessingQueue = OperationQueue(maxConcurrentCount: 2) - - /// Image decompressing queue. Default maximum concurrent task count is 2. - public var imageDecompressingQueue = OperationQueue(maxConcurrentCount: 2) - - // MARK: - Initializer - - /// Instantiates a pipeline configuration. - /// - /// - parameter dataLoader: `DataLoader()` by default. - public init(dataLoader: any DataLoading = DataLoader()) { - self.dataLoader = dataLoader - } - - // MARK: - Predefined Configurations - - /// A configuration with an HTTP disk cache (`URLCache`) with a size limit - /// of 150 MB. This is a default configuration. - /// - /// Also uses ``ImageCache/shared`` for in-memory caching with the size - /// that adjusts bsed on the amount of device memory. - public static var withURLCache: Configuration { Configuration() } - - /// A configuration with an aggressive disk cache (``DataCache``) with a - /// size limit of 150 MB. An HTTP cache (`URLCache`) is disabled. - /// - /// Also uses ``ImageCache/shared`` for in-memory caching with the size - /// that adjusts bsed on the amount of device memory. - public static var withDataCache: Configuration { - withDataCache() - } - - /// A configuration with an aggressive disk cache (``DataCache``) with a - /// size limit of 150 MB by default. An HTTP cache (`URLCache`) is disabled. - /// - /// Also uses ``ImageCache/shared`` for in-memory caching with the size - /// that adjusts bsed on the amount of device memory. - /// - /// - parameters: - /// - name: Data cache name. - /// - sizeLimit: Size limit, by default 150 MB. - public static func withDataCache( - name: String = "com.github.kean.Nuke.DataCache", - sizeLimit: Int? = nil - ) -> Configuration { - let dataLoader: DataLoader = { - let config = URLSessionConfiguration.default - config.urlCache = nil - return DataLoader(configuration: config) - }() - - var config = Configuration() - config.dataLoader = dataLoader - - let dataCache = try? DataCache(name: name) - if let sizeLimit { - dataCache?.sizeLimit = sizeLimit - } - config.dataCache = dataCache - - return config - } - } - - /// Determines what images are stored in the disk cache. - public enum DataCachePolicy: Sendable { - /// Store original image data for requests with no processors. Store - /// _only_ processed images for requests with processors. - /// - /// - note: Store only processed images for local resources (file:// or - /// data:// URL scheme). - /// - /// - important: With this policy, the pipeline's ``ImagePipeline/loadData(with:completion:)-6cwk3`` - /// method will not store the images in the disk cache for requests with - /// any processors applied – this method only loads data and doesn't - /// decode images. - case automatic - - /// Store only original image data. - /// - /// - note: If the resource is local (file:// or data:// URL scheme), - /// data isn't stored. - case storeOriginalData - - /// Encode and store images. - /// - /// - note: This is useful if you want to store images in a format - /// different than provided by a server, e.g. decompressed. In other - /// scenarios, consider using ``automatic`` policy instead. - /// - /// - important: With this policy, the pipeline's ``ImagePipeline/loadData(with:completion:)-6cwk3`` - /// method will not store the images in the disk cache – this method only - /// loads data and doesn't decode images. - case storeEncodedImages - - /// Stores both processed images and the original image data. - /// - /// - note: If the resource is local (has file:// or data:// scheme), - /// only the processed images are stored. - case storeAll - } -} diff --git a/Nuke/Pipeline/ImagePipelineDelegate.swift b/Nuke/Pipeline/ImagePipelineDelegate.swift deleted file mode 100644 index 7f19c58..0000000 --- a/Nuke/Pipeline/ImagePipelineDelegate.swift +++ /dev/null @@ -1,141 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// A delegate that allows you to customize the pipeline dynamically on a per-request basis. -/// -/// - important: The delegate methods are performed on the pipeline queue in the -/// background. -public protocol ImagePipelineDelegate: AnyObject, Sendable { - // MARK: Configuration - - /// Returns data loader for the given request. - func dataLoader(for request: ImageRequest, pipeline: ImagePipeline) -> any DataLoading - - /// Returns image decoder for the given context. - func imageDecoder(for context: ImageDecodingContext, pipeline: ImagePipeline) -> (any ImageDecoding)? - - /// Returns image encoder for the given context. - func imageEncoder(for context: ImageEncodingContext, pipeline: ImagePipeline) -> any ImageEncoding - - // MARK: Caching - - /// Returns in-memory image cache for the given request. Return `nil` to prevent cache reads and writes. - func imageCache(for request: ImageRequest, pipeline: ImagePipeline) -> (any ImageCaching)? - - /// Returns disk cache for the given request. Return `nil` to prevent cache - /// reads and writes. - func dataCache(for request: ImageRequest, pipeline: ImagePipeline) -> (any DataCaching)? - - /// Returns a cache key identifying the image produced for the given request - /// (including image processors). The key is used for both in-memory and - /// on-disk caches. - /// - /// Return `nil` to use a default key. - func cacheKey(for request: ImageRequest, pipeline: ImagePipeline) -> String? - - /// Gets called when the pipeline is about to save data for the given request. - /// The implementation must call the completion closure passing `non-nil` data - /// to enable caching or `nil` to prevent it. - /// - /// This method calls only if the request parameters and data caching policy - /// of the pipeline already allow caching. - /// - /// - parameters: - /// - data: Either the original data or the encoded image in case of storing - /// a processed or re-encoded image. - /// - image: Non-nil in case storing an encoded image. - /// - request: The request for which image is being stored. - /// - completion: The implementation must call the completion closure - /// passing `non-nil` data to enable caching or `nil` to prevent it. You can - /// safely call it synchronously. The callback gets called on the background - /// thread. - func willCache(data: Data, image: ImageContainer?, for request: ImageRequest, pipeline: ImagePipeline, completion: @escaping (Data?) -> Void) - - // MARK: Decompression - - func shouldDecompress(response: ImageResponse, for request: ImageRequest, pipeline: ImagePipeline) -> Bool - - func decompress(response: ImageResponse, request: ImageRequest, pipeline: ImagePipeline) -> ImageResponse - - // MARK: ImageTaskDelegate - - /// Gets called when the task is created. Unlike other methods, it is called - /// immediately on the caller's queue. - func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline) - - /// Gets called when the task is started. The caller can save the instance - /// of the class to update the task later. - func imageTaskDidStart(_ task: ImageTask, pipeline: ImagePipeline) - - /// Gets called when the progress is updated. - func imageTask(_ task: ImageTask, didUpdateProgress progress: ImageTask.Progress, pipeline: ImagePipeline) - - /// Gets called when a new progressive image is produced. - func imageTask(_ task: ImageTask, didReceivePreview response: ImageResponse, pipeline: ImagePipeline) - - /// Gets called when the task is cancelled. - /// - /// - important: This doesn't get called immediately. - func imageTaskDidCancel(_ task: ImageTask, pipeline: ImagePipeline) - - /// If you cancel the task from the same queue as the callback queue, this - /// callback is guaranteed not to be called. - func imageTask(_ task: ImageTask, didCompleteWithResult result: Result, pipeline: ImagePipeline) -} - -extension ImagePipelineDelegate { - public func imageCache(for request: ImageRequest, pipeline: ImagePipeline) -> (any ImageCaching)? { - pipeline.configuration.imageCache - } - - public func dataLoader(for request: ImageRequest, pipeline: ImagePipeline) -> any DataLoading { - pipeline.configuration.dataLoader - } - - public func dataCache(for request: ImageRequest, pipeline: ImagePipeline) -> (any DataCaching)? { - pipeline.configuration.dataCache - } - - public func imageDecoder(for context: ImageDecodingContext, pipeline: ImagePipeline) -> (any ImageDecoding)? { - pipeline.configuration.makeImageDecoder(context) - } - - public func imageEncoder(for context: ImageEncodingContext, pipeline: ImagePipeline) -> any ImageEncoding { - pipeline.configuration.makeImageEncoder(context) - } - - public func cacheKey(for request: ImageRequest, pipeline: ImagePipeline) -> String? { - nil - } - - public func willCache(data: Data, image: ImageContainer?, for request: ImageRequest, pipeline: ImagePipeline, completion: @escaping (Data?) -> Void) { - completion(data) - } - - public func shouldDecompress(response: ImageResponse, for request: ImageRequest, pipeline: ImagePipeline) -> Bool { - pipeline.configuration.isDecompressionEnabled - } - - public func decompress(response: ImageResponse, request: ImageRequest, pipeline: ImagePipeline) -> ImageResponse { - var response = response - response.container.image = ImageDecompression.decompress(image: response.image, isUsingPrepareForDisplay: pipeline.configuration.isUsingPrepareForDisplay) - return response - } - - public func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline) {} - - public func imageTaskDidStart(_ task: ImageTask, pipeline: ImagePipeline) {} - - public func imageTask(_ task: ImageTask, didUpdateProgress progress: ImageTask.Progress, pipeline: ImagePipeline) {} - - public func imageTask(_ task: ImageTask, didReceivePreview response: ImageResponse, pipeline: ImagePipeline) {} - - public func imageTaskDidCancel(_ task: ImageTask, pipeline: ImagePipeline) {} - - public func imageTask(_ task: ImageTask, didCompleteWithResult result: Result, pipeline: ImagePipeline) {} -} - -final class ImagePipelineDefaultDelegate: ImagePipelineDelegate {} diff --git a/Nuke/Pipeline/ImagePipelineError.swift b/Nuke/Pipeline/ImagePipelineError.swift deleted file mode 100644 index 0e0152a..0000000 --- a/Nuke/Pipeline/ImagePipelineError.swift +++ /dev/null @@ -1,65 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -extension ImagePipeline { - /// Represents all possible image pipeline errors. - public enum Error: Swift.Error, CustomStringConvertible, @unchecked Sendable { - /// Returned if data not cached and ``ImageRequest/Options-swift.struct/returnCacheDataDontLoad`` option is specified. - case dataMissingInCache - /// Data loader failed to load image data with a wrapped error. - case dataLoadingFailed(error: Swift.Error) - /// Data loader returned empty data. - case dataIsEmpty - /// No decoder registered for the given data. - /// - /// This error can only be thrown if the pipeline has custom decoders. - /// By default, the pipeline uses ``ImageDecoders/Default`` as a catch-all. - case decoderNotRegistered(context: ImageDecodingContext) - /// Decoder failed to produce a final image. - case decodingFailed(decoder: any ImageDecoding, context: ImageDecodingContext, error: Swift.Error) - /// Processor failed to produce a final image. - case processingFailed(processor: any ImageProcessing, context: ImageProcessingContext, error: Swift.Error) - /// Load image method was called with no image request. - case imageRequestMissing - /// Image pipeline is invalidated and no requests can be made. - case pipelineInvalidated - } -} - -extension ImagePipeline.Error { - /// Returns underlying data loading error. - public var dataLoadingError: Swift.Error? { - switch self { - case .dataLoadingFailed(let error): - return error - default: - return nil - } - } - - public var description: String { - switch self { - case .dataMissingInCache: - return "Failed to load data from cache and download is disabled." - case let .dataLoadingFailed(error): - return "Failed to load image data. Underlying error: \(error)." - case .dataIsEmpty: - return "Data loader returned empty data." - case .decoderNotRegistered: - return "No decoders registered for the downloaded data." - case let .decodingFailed(decoder, _, error): - let underlying = error is ImageDecodingError ? "" : " Underlying error: \(error)." - return "Failed to decode image data using decoder \(decoder).\(underlying)" - case let .processingFailed(processor, _, error): - let underlying = error is ImageProcessingError ? "" : " Underlying error: \(error)." - return "Failed to process the image using processor \(processor).\(underlying)" - case .imageRequestMissing: - return "Load image method was called with no image request or no URL." - case .pipelineInvalidated: - return "Image pipeline is invalidated and no requests can be made." - } - } -} diff --git a/Nuke/Prefetching/ImagePrefetcher.swift b/Nuke/Prefetching/ImagePrefetcher.swift deleted file mode 100644 index 88cb4ed..0000000 --- a/Nuke/Prefetching/ImagePrefetcher.swift +++ /dev/null @@ -1,233 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// Prefetches and caches images to eliminate delays when requesting the same -/// images later. -/// -/// The prefetcher cancels all of the outstanding tasks when deallocated. -/// -/// All ``ImagePrefetcher`` methods are thread-safe and are optimized to be used -/// even from the main thread during scrolling. -public final class ImagePrefetcher: @unchecked Sendable { - /// Pauses the prefetching. - /// - /// - note: When you pause, the prefetcher will finish outstanding tasks - /// (by default, there are only 2 at a time), and pause the rest. - public var isPaused: Bool = false { - didSet { queue.isSuspended = isPaused } - } - - /// The priority of the requests. By default, ``ImageRequest/Priority-swift.enum/low``. - /// - /// Changing the priority also changes the priority of all of the outstanding - /// tasks managed by the prefetcher. - public var priority: ImageRequest.Priority = .low { - didSet { - let newValue = priority - pipeline.queue.async { self.didUpdatePriority(to: newValue) } - } - } - - /// Prefetching destination. - public enum Destination: Sendable { - /// Prefetches the image and stores it in both the memory and the disk - /// cache (make sure to enable it). - case memoryCache - - /// Prefetches the image data and stores it in disk caches. It does not - /// require decoding the image data and therefore requires less CPU. - /// - /// - important: This option is incompatible with ``ImagePipeline/DataCachePolicy/automatic`` - /// (for requests with processors) and ``ImagePipeline/DataCachePolicy/storeEncodedImages``. - case diskCache - } - - /// The closure that gets called when the prefetching completes for all the - /// scheduled requests. The closure is always called on completion, - /// regardless of whether the requests succeed or some fail. - /// - /// - note: The closure is called on the main queue. - public var didComplete: (() -> Void)? - - private let pipeline: ImagePipeline - private var tasks = [ImageLoadKey: Task]() - private let destination: Destination - private var _priority: ImageRequest.Priority = .low - let queue = OperationQueue() // internal for testing - - /// Initializes the ``ImagePrefetcher`` instance. - /// - /// - parameters: - /// - pipeline: The pipeline used for loading images. - /// - destination: By default load images in all cache layers. - /// - maxConcurrentRequestCount: 2 by default. - public init(pipeline: ImagePipeline = ImagePipeline.shared, - destination: Destination = .memoryCache, - maxConcurrentRequestCount: Int = 2) { - self.pipeline = pipeline - self.destination = destination - self.queue.maxConcurrentOperationCount = maxConcurrentRequestCount - self.queue.underlyingQueue = pipeline.queue - } - - deinit { - let tasks = self.tasks.values // Make sure we don't retain self - self.tasks.removeAll() - - pipeline.queue.async { - for task in tasks { - task.cancel() - } - } - } - - /// Starts prefetching images for the given URL. - /// - /// See also ``startPrefetching(with:)-718dg`` that works with ``ImageRequest``. - public func startPrefetching(with urls: [URL]) { - startPrefetching(with: urls.map { ImageRequest(url: $0) }) - } - - /// Starts prefetching images for the given requests. - /// - /// When you need to display the same image later, use the ``ImagePipeline`` - /// or the view extensions to load it as usual. The pipeline will take care - /// of coalescing the requests to avoid any duplicate work. - /// - /// The priority of the requests is set to the priority of the prefetcher - /// (`.low` by default). - /// - /// See also ``startPrefetching(with:)-1jef2`` that works with `URL`. - public func startPrefetching(with requests: [ImageRequest]) { - pipeline.queue.async { - self._startPrefetching(with: requests) - } - } - - public func _startPrefetching(with requests: [ImageRequest]) { - for request in requests { - var request = request - if _priority != request.priority { - request.priority = _priority - } - _startPrefetching(with: request) - } - sendCompletionIfNeeded() - } - - private func _startPrefetching(with request: ImageRequest) { - guard pipeline.cache[request] == nil else { - return - } - let key = request.makeImageLoadKey() - guard tasks[key] == nil else { - return - } - let task = Task(request: request, key: key) - task.operation = queue.add { [weak self] finish in - guard let self else { return finish() } - self.loadImage(task: task, finish: finish) - } - tasks[key] = task - return - } - - private func loadImage(task: Task, finish: @escaping () -> Void) { - switch destination { - case .diskCache: - task.imageTask = pipeline.loadData(with: task.request, isConfined: true, queue: pipeline.queue, progress: nil) { [weak self] _ in - self?._remove(task) - finish() - } - case .memoryCache: - task.imageTask = pipeline.loadImage(with: task.request, isConfined: true, queue: pipeline.queue, progress: nil) { [weak self] _ in - self?._remove(task) - finish() - } - } - task.onCancelled = finish - } - - private func _remove(_ task: Task) { - guard tasks[task.key] === task else { return } // Should never happen - tasks[task.key] = nil - sendCompletionIfNeeded() - } - - private func sendCompletionIfNeeded() { - guard tasks.isEmpty, let callback = didComplete else { - return - } - DispatchQueue.main.async(execute: callback) - } - - /// Stops prefetching images for the given URLs and cancels outstanding - /// requests. - /// - /// See also ``stopPrefetching(with:)-8cdam`` that works with ``ImageRequest``. - public func stopPrefetching(with urls: [URL]) { - stopPrefetching(with: urls.map { ImageRequest(url: $0) }) - } - - /// Stops prefetching images for the given requests and cancels outstanding - /// requests. - /// - /// You don't need to balance the number of `start` and `stop` requests. - /// If you have multiple screens with prefetching, create multiple instances - /// of ``ImagePrefetcher``. - /// - /// See also ``stopPrefetching(with:)-2tcyq`` that works with `URL`. - public func stopPrefetching(with requests: [ImageRequest]) { - pipeline.queue.async { - for request in requests { - self._stopPrefetching(with: request) - } - } - } - - private func _stopPrefetching(with request: ImageRequest) { - if let task = tasks.removeValue(forKey: request.makeImageLoadKey()) { - task.cancel() - } - } - - /// Stops all prefetching tasks. - public func stopPrefetching() { - pipeline.queue.async { - self.tasks.values.forEach { $0.cancel() } - self.tasks.removeAll() - } - } - - private func didUpdatePriority(to priority: ImageRequest.Priority) { - guard _priority != priority else { return } - _priority = priority - for task in tasks.values { - task.imageTask?.priority = priority - } - } - - private final class Task: @unchecked Sendable { - let key: ImageLoadKey - let request: ImageRequest - weak var imageTask: ImageTask? - weak var operation: Operation? - var onCancelled: (() -> Void)? - - init(request: ImageRequest, key: ImageLoadKey) { - self.request = request - self.key = key - } - - // When task is cancelled, it is removed from the prefetcher and can - // never get cancelled twice. - func cancel() { - operation?.cancel() - imageTask?.cancel() - onCancelled?() - } - } -} diff --git a/Nuke/Processing/ImageDecompression.swift b/Nuke/Processing/ImageDecompression.swift deleted file mode 100644 index 83e2e77..0000000 --- a/Nuke/Processing/ImageDecompression.swift +++ /dev/null @@ -1,24 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -enum ImageDecompression { - - static func decompress(image: PlatformImage, isUsingPrepareForDisplay: Bool = false) -> PlatformImage { - image.decompressed(isUsingPrepareForDisplay: isUsingPrepareForDisplay) ?? image - } - - // MARK: Managing Decompression State - - static var isDecompressionNeededAK: UInt8 = 0 - - static func setDecompressionNeeded(_ isDecompressionNeeded: Bool, for image: PlatformImage) { - objc_setAssociatedObject(image, &isDecompressionNeededAK, isDecompressionNeeded, .OBJC_ASSOCIATION_RETAIN) - } - - static func isDecompressionNeeded(for image: PlatformImage) -> Bool? { - objc_getAssociatedObject(image, &isDecompressionNeededAK) as? Bool - } -} diff --git a/Nuke/Processing/ImageProcessing.swift b/Nuke/Processing/ImageProcessing.swift deleted file mode 100644 index 21d3d7c..0000000 --- a/Nuke/Processing/ImageProcessing.swift +++ /dev/null @@ -1,107 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -#if !os(macOS) -import UIKit -#else -import AppKit -#endif - -/// Performs image processing. -/// -/// For basic processing needs, implement the following method: -/// -/// ```swift -/// func process(image: PlatformImage) -> PlatformImage? -/// ``` -/// -/// If your processor needs to manipulate image metadata (``ImageContainer``), or -/// get access to more information via the context (``ImageProcessingContext``), -/// there is an additional method that allows you to do that: -/// -/// ```swift -/// func process(image container: ImageContainer, context: ImageProcessingContext) -> ImageContainer? -/// ``` -/// -/// You must implement either one of those methods. -public protocol ImageProcessing: Sendable { - /// Returns a processed image. By default, returns `nil`. - /// - /// - note: Gets called a background queue managed by the pipeline. - func process(_ image: PlatformImage) -> PlatformImage? - - /// Optional method. Returns a processed image. By default, this calls the - /// basic `process(image:)` method. - /// - /// - note: Gets called a background queue managed by the pipeline. - func process(_ container: ImageContainer, context: ImageProcessingContext) throws -> ImageContainer - - /// Returns a string that uniquely identifies the processor. - /// - /// Consider using the reverse DNS notation. - var identifier: String { get } - - /// Returns a unique processor identifier. - /// - /// The default implementation simply returns `var identifier: String` but - /// can be overridden as a performance optimization - creating and comparing - /// strings is _expensive_ so you can opt-in to return something which is - /// fast to create and to compare. See ``ImageProcessors/Resize`` for an example. - /// - /// - note: A common approach is to make your processor `Hashable` and return `self` - /// as a hashable identifier. - var hashableIdentifier: AnyHashable { get } -} - -extension ImageProcessing { - /// The default implementation simply calls the basic - /// `process(_ image: PlatformImage) -> PlatformImage?` method. - public func process(_ container: ImageContainer, context: ImageProcessingContext) throws -> ImageContainer { - guard let output = process(container.image) else { - throw ImageProcessingError.unknown - } - var container = container - container.image = output - return container - } - - /// The default impleemntation simply returns `var identifier: String`. - public var hashableIdentifier: AnyHashable { identifier } -} - -extension ImageProcessing where Self: Hashable { - public var hashableIdentifier: AnyHashable { self } -} - -/// Image processing context used when selecting which processor to use. -public struct ImageProcessingContext: Sendable { - public var request: ImageRequest - public var response: ImageResponse - public var isCompleted: Bool - - public init(request: ImageRequest, response: ImageResponse, isCompleted: Bool) { - self.request = request - self.response = response - self.isCompleted = isCompleted - } -} - -public enum ImageProcessingError: Error, CustomStringConvertible, Sendable { - case unknown - - public var description: String { "Unknown" } -} - -func == (lhs: [any ImageProcessing], rhs: [any ImageProcessing]) -> Bool { - guard lhs.count == rhs.count else { - return false - } - // Lazily creates `hashableIdentifiers` because for some processors the - // identifiers might be expensive to compute. - return zip(lhs, rhs).allSatisfy { - $0.hashableIdentifier == $1.hashableIdentifier - } -} diff --git a/Nuke/Processing/ImageProcessingOptions.swift b/Nuke/Processing/ImageProcessingOptions.swift deleted file mode 100644 index 2ace3a6..0000000 --- a/Nuke/Processing/ImageProcessingOptions.swift +++ /dev/null @@ -1,86 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -#if canImport(UIKit) -import UIKit -#endif - -#if canImport(AppKit) -import AppKit -#endif - -/// A namespace with shared image processing options. -public enum ImageProcessingOptions: Sendable { - - public enum Unit: CustomStringConvertible, Sendable { - case points - case pixels - - public var description: String { - switch self { - case .points: return "points" - case .pixels: return "pixels" - } - } - } - - /// Draws a border. - /// - /// - important: To make sure that the border looks the way you expect, - /// make sure that the images you display exactly match the size of the - /// views in which they get displayed. If you can't guarantee that, pleasee - /// consider adding border to a view layer. This should be your primary - /// option regardless. - public struct Border: Hashable, CustomStringConvertible, @unchecked Sendable { - public let width: CGFloat - -#if canImport(UIKit) - public let color: UIColor - - /// - parameters: - /// - color: Border color. - /// - width: Border width. - /// - unit: Unit of the width. - public init(color: UIColor, width: CGFloat = 1, unit: Unit = .points) { - self.color = color - self.width = width.converted(to: unit) - } -#else - public let color: NSColor - - /// - parameters: - /// - color: Border color. - /// - width: Border width. - /// - unit: Unit of the width. - public init(color: NSColor, width: CGFloat = 1, unit: Unit = .points) { - self.color = color - self.width = width.converted(to: unit) - } -#endif - - public var description: String { - "Border(color: \(color.hex), width: \(width) pixels)" - } - } - - /// An option for how to resize the image. - public enum ContentMode: CustomStringConvertible, Sendable { - /// Scales the image so that it completely fills the target area. - /// Maintains the aspect ratio of the original image. - case aspectFill - - /// Scales the image so that it fits the target size. Maintains the - /// aspect ratio of the original image. - case aspectFit - - public var description: String { - switch self { - case .aspectFill: return ".aspectFill" - case .aspectFit: return ".aspectFit" - } - } - } -} diff --git a/Nuke/Processing/ImageProcessors+Anonymous.swift b/Nuke/Processing/ImageProcessors+Anonymous.swift deleted file mode 100644 index 105096d..0000000 --- a/Nuke/Processing/ImageProcessors+Anonymous.swift +++ /dev/null @@ -1,32 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -#if !os(macOS) -import UIKit -#else -import AppKit -#endif - -extension ImageProcessors { - /// Processed an image using a specified closure. - public struct Anonymous: ImageProcessing, CustomStringConvertible { - public let identifier: String - private let closure: @Sendable (PlatformImage) -> PlatformImage? - - public init(id: String, _ closure: @Sendable @escaping (PlatformImage) -> PlatformImage?) { - self.identifier = id - self.closure = closure - } - - public func process(_ image: PlatformImage) -> PlatformImage? { - closure(image) - } - - public var description: String { - "AnonymousProcessor(identifier: \(identifier)" - } - } -} diff --git a/Nuke/Processing/ImageProcessors+Circle.swift b/Nuke/Processing/ImageProcessors+Circle.swift deleted file mode 100644 index cd79051..0000000 --- a/Nuke/Processing/ImageProcessors+Circle.swift +++ /dev/null @@ -1,38 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -#if !os(macOS) -import UIKit -#else -import AppKit -#endif - -extension ImageProcessors { - - /// Rounds the corners of an image into a circle. If the image is not a square, - /// crops it to a square first. - public struct Circle: ImageProcessing, Hashable, CustomStringConvertible { - private let border: ImageProcessingOptions.Border? - - /// - parameter border: `nil` by default. - public init(border: ImageProcessingOptions.Border? = nil) { - self.border = border - } - - public func process(_ image: PlatformImage) -> PlatformImage? { - image.processed.byDrawingInCircle(border: border) - } - - public var identifier: String { - let suffix = border.map { "?border=\($0)" } - return "com.github.kean/nuke/circle" + (suffix ?? "") - } - - public var description: String { - "Circle(border: \(border?.description ?? "nil"))" - } - } -} diff --git a/Nuke/Processing/ImageProcessors+Composition.swift b/Nuke/Processing/ImageProcessors+Composition.swift deleted file mode 100644 index 56d7e27..0000000 --- a/Nuke/Processing/ImageProcessors+Composition.swift +++ /dev/null @@ -1,67 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -#if !os(macOS) -import UIKit -#else -import AppKit -#endif - -extension ImageProcessors { - /// Composes multiple processors. - public struct Composition: ImageProcessing, Hashable, CustomStringConvertible { - let processors: [any ImageProcessing] - - /// Composes multiple processors. - public init(_ processors: [any ImageProcessing]) { - // note: multiple compositions are not flatten by default. - self.processors = processors - } - - /// Processes the given image by applying each processor in an order in - /// which they were added. If one of the processors fails to produce - /// an image the processing stops and `nil` is returned. - public func process(_ image: PlatformImage) -> PlatformImage? { - processors.reduce(image) { image, processor in - autoreleasepool { - image.flatMap(processor.process) - } - } - } - - /// Processes the given image by applying each processor in an order in - /// which they were added. If one of the processors fails to produce - /// an image the processing stops and an error is thrown. - public func process(_ container: ImageContainer, context: ImageProcessingContext) throws -> ImageContainer { - try processors.reduce(container) { container, processor in - try autoreleasepool { - try processor.process(container, context: context) - } - } - } - - /// Returns combined identifier of all the underlying processors. - public var identifier: String { - processors.map({ $0.identifier }).joined() - } - - /// Creates a combined hash of all the given processors. - public func hash(into hasher: inout Hasher) { - for processor in processors { - hasher.combine(processor.hashableIdentifier) - } - } - - /// Compares all the underlying processors for equality. - public static func == (lhs: Composition, rhs: Composition) -> Bool { - lhs.processors == rhs.processors - } - - public var description: String { - "Composition(processors: \(processors))" - } - } -} diff --git a/Nuke/Processing/ImageProcessors+CoreImage.swift b/Nuke/Processing/ImageProcessors+CoreImage.swift deleted file mode 100644 index 089bfc2..0000000 --- a/Nuke/Processing/ImageProcessors+CoreImage.swift +++ /dev/null @@ -1,119 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) - -import Foundation -import CoreImage - -#if !os(macOS) -import UIKit -#else -import AppKit -#endif - -extension ImageProcessors { - - /// Applies Core Image filter (`CIFilter`) to the image. - /// - /// # Performance Considerations. - /// - /// Prefer chaining multiple `CIFilter` objects using `Core Image` facilities - /// instead of using multiple instances of `ImageProcessors.CoreImageFilter`. - /// - /// # References - /// - /// - [Core Image Programming Guide](https://developer.apple.com/library/ios/documentation/GraphicsImaging/Conceptual/CoreImaging/ci_intro/ci_intro.html) - /// - [Core Image Filter Reference](https://developer.apple.com/library/prerelease/ios/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html) - public struct CoreImageFilter: ImageProcessing, CustomStringConvertible, @unchecked Sendable { - public let name: String - public let parameters: [String: Any] - public let identifier: String - - /// - parameter identifier: Uniquely identifies the processor. - public init(name: String, parameters: [String: Any], identifier: String) { - self.name = name - self.parameters = parameters - self.identifier = identifier - } - - public init(name: String) { - self.name = name - self.parameters = [:] - self.identifier = "com.github.kean/nuke/core_image?name=\(name))" - } - - public func process(_ image: PlatformImage) -> PlatformImage? { - try? _process(image) - } - - public func process(_ container: ImageContainer, context: ImageProcessingContext) throws -> ImageContainer { - try container.map(_process) - } - - private func _process(_ image: PlatformImage) throws -> PlatformImage { - try CoreImageFilter.applyFilter(named: name, parameters: parameters, to: image) - } - - // MARK: - Apply Filter - - /// A default context shared between all Core Image filters. The context - /// has `.priorityRequestLow` option set to `true`. - public static var context = CIContext(options: [.priorityRequestLow: true]) - - static func applyFilter(named name: String, parameters: [String: Any] = [:], to image: PlatformImage) throws -> PlatformImage { - guard let filter = CIFilter(name: name, parameters: parameters) else { - throw Error.failedToCreateFilter(name: name, parameters: parameters) - } - return try CoreImageFilter.apply(filter: filter, to: image) - } - - /// Applies filter to the given image. - public static func apply(filter: CIFilter, to image: PlatformImage) throws -> PlatformImage { - func getCIImage() throws -> CoreImage.CIImage { - if let image = image.ciImage { - return image - } - if let image = image.cgImage { - return CoreImage.CIImage(cgImage: image) - } - throw Error.inputImageIsEmpty(inputImage: image) - } - filter.setValue(try getCIImage(), forKey: kCIInputImageKey) - guard let outputImage = filter.outputImage else { - throw Error.failedToApplyFilter(filter: filter) - } - guard let imageRef = context.createCGImage(outputImage, from: outputImage.extent) else { - throw Error.failedToCreateOutputCGImage(image: outputImage) - } - return PlatformImage.make(cgImage: imageRef, source: image) - } - - public var description: String { - "CoreImageFilter(name: \(name), parameters: \(parameters))" - } - - public enum Error: Swift.Error, CustomStringConvertible { - case failedToCreateFilter(name: String, parameters: [String: Any]) - case inputImageIsEmpty(inputImage: PlatformImage) - case failedToApplyFilter(filter: CIFilter) - case failedToCreateOutputCGImage(image: CIImage) - - public var description: String { - switch self { - case let .failedToCreateFilter(name, parameters): - return "Failed to create filter named \(name) with parameters: \(parameters)" - case let .inputImageIsEmpty(inputImage): - return "Failed to create input CIImage for \(inputImage)" - case let .failedToApplyFilter(filter): - return "Failed to apply filter: \(filter.name)" - case let .failedToCreateOutputCGImage(image): - return "Failed to create output image for extent: \(image.extent) from \(image)" - } - } - } - } -} - -#endif diff --git a/Nuke/Processing/ImageProcessors+GaussianBlur.swift b/Nuke/Processing/ImageProcessors+GaussianBlur.swift deleted file mode 100644 index 24c6a55..0000000 --- a/Nuke/Processing/ImageProcessors+GaussianBlur.swift +++ /dev/null @@ -1,52 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) - -import Foundation -import CoreImage - -#if !os(macOS) -import UIKit -#else -import AppKit -#endif - -extension ImageProcessors { - /// Blurs an image using `CIGaussianBlur` filter. - public struct GaussianBlur: ImageProcessing, Hashable, CustomStringConvertible { - private let radius: Int - - /// Initializes the receiver with a blur radius. - /// - /// - parameter radius: `8` by default. - public init(radius: Int = 8) { - self.radius = radius - } - - /// Applies `CIGaussianBlur` filter to the image. - public func process(_ image: PlatformImage) -> PlatformImage? { - try? _process(image) - } - - /// Applies `CIGaussianBlur` filter to the image. - public func process(_ container: ImageContainer, context: ImageProcessingContext) throws -> ImageContainer { - try container.map(_process(_:)) - } - - private func _process(_ image: PlatformImage) throws -> PlatformImage { - try CoreImageFilter.applyFilter(named: "CIGaussianBlur", parameters: ["inputRadius": radius], to: image) - } - - public var identifier: String { - "com.github.kean/nuke/gaussian_blur?radius=\(radius)" - } - - public var description: String { - "GaussianBlur(radius: \(radius))" - } - } -} - -#endif diff --git a/Nuke/Processing/ImageProcessors+Resize.swift b/Nuke/Processing/ImageProcessors+Resize.swift deleted file mode 100644 index 984136c..0000000 --- a/Nuke/Processing/ImageProcessors+Resize.swift +++ /dev/null @@ -1,96 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation -import CoreGraphics - -#if !os(macOS) -import UIKit -#else -import AppKit -#endif - -extension ImageProcessors { - /// Scales an image to a specified size. - public struct Resize: ImageProcessing, Hashable, CustomStringConvertible { - private let size: ImageTargetSize - private let contentMode: ImageProcessingOptions.ContentMode - private let crop: Bool - private let upscale: Bool - - // Deprecated in Nuke 12.0 - @available(*, deprecated, message: "Renamed to `ImageProcessingOptions.ContentMode") - public typealias ContentMode = ImageProcessingOptions.ContentMode - - /// Initializes the processor with the given size. - /// - /// - parameters: - /// - size: The target size. - /// - unit: Unit of the target size. - /// - contentMode: A target content mode. - /// - crop: If `true` will crop the image to match the target size. - /// Does nothing with content mode .aspectFill. - /// - upscale: By default, upscaling is not allowed. - public init(size: CGSize, unit: ImageProcessingOptions.Unit = .points, contentMode: ImageProcessingOptions.ContentMode = .aspectFill, crop: Bool = false, upscale: Bool = false) { - self.size = ImageTargetSize(size: size, unit: unit) - self.contentMode = contentMode - self.crop = crop - self.upscale = upscale - } - - /// Scales an image to the given width preserving aspect ratio. - /// - /// - parameters: - /// - width: The target width. - /// - unit: Unit of the target size. - /// - upscale: `false` by default. - public init(width: CGFloat, unit: ImageProcessingOptions.Unit = .points, upscale: Bool = false) { - self.init(size: CGSize(width: width, height: 9999), unit: unit, contentMode: .aspectFit, crop: false, upscale: upscale) - } - - /// Scales an image to the given height preserving aspect ratio. - /// - /// - parameters: - /// - height: The target height. - /// - unit: Unit of the target size. - /// - upscale: By default, upscaling is not allowed. - public init(height: CGFloat, unit: ImageProcessingOptions.Unit = .points, upscale: Bool = false) { - self.init(size: CGSize(width: 9999, height: height), unit: unit, contentMode: .aspectFit, crop: false, upscale: upscale) - } - - public func process(_ image: PlatformImage) -> PlatformImage? { - if crop && contentMode == .aspectFill { - return image.processed.byResizingAndCropping(to: size.cgSize) - } - return image.processed.byResizing(to: size.cgSize, contentMode: contentMode, upscale: upscale) - } - - public var identifier: String { - "com.github.kean/nuke/resize?s=\(size.cgSize),cm=\(contentMode),crop=\(crop),upscale=\(upscale)" - } - - public var description: String { - "Resize(size: \(size.cgSize) pixels, contentMode: \(contentMode), crop: \(crop), upscale: \(upscale))" - } - } -} - -// Adds Hashable without making changes to public CGSize API -struct ImageTargetSize: Hashable { - let cgSize: CGSize - - /// Creates the size in pixels by scaling to the input size to the screen scale - /// if needed. - init(size: CGSize, unit: ImageProcessingOptions.Unit) { - switch unit { - case .pixels: self.cgSize = size // The size is already in pixels - case .points: self.cgSize = size.scaled(by: Screen.scale) - } - } - - func hash(into hasher: inout Hasher) { - hasher.combine(cgSize.width) - hasher.combine(cgSize.height) - } -} diff --git a/Nuke/Processing/ImageProcessors+RoundedCorners.swift b/Nuke/Processing/ImageProcessors+RoundedCorners.swift deleted file mode 100644 index 1ec97f5..0000000 --- a/Nuke/Processing/ImageProcessors+RoundedCorners.swift +++ /dev/null @@ -1,47 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation -import CoreGraphics - -#if !os(macOS) -import UIKit -#else -import AppKit -#endif - -extension ImageProcessors { - /// Rounds the corners of an image to the specified radius. - /// - /// - important: In order for the corners to be displayed correctly, the image must exactly match the size - /// of the image view in which it will be displayed. See ``ImageProcessors/Resize`` for more info. - public struct RoundedCorners: ImageProcessing, Hashable, CustomStringConvertible { - private let radius: CGFloat - private let border: ImageProcessingOptions.Border? - - /// Initializes the processor with the given radius. - /// - /// - parameters: - /// - radius: The radius of the corners. - /// - unit: Unit of the radius. - /// - border: An optional border drawn around the image. - public init(radius: CGFloat, unit: ImageProcessingOptions.Unit = .points, border: ImageProcessingOptions.Border? = nil) { - self.radius = radius.converted(to: unit) - self.border = border - } - - public func process(_ image: PlatformImage) -> PlatformImage? { - image.processed.byAddingRoundedCorners(radius: radius, border: border) - } - - public var identifier: String { - let suffix = border.map { ",border=\($0)" } - return "com.github.kean/nuke/rounded_corners?radius=\(radius)" + (suffix ?? "") - } - - public var description: String { - "RoundedCorners(radius: \(radius) pixels, border: \(border?.description ?? "nil"))" - } - } -} diff --git a/Nuke/Processing/ImageProcessors.swift b/Nuke/Processing/ImageProcessors.swift deleted file mode 100644 index db1f6c5..0000000 --- a/Nuke/Processing/ImageProcessors.swift +++ /dev/null @@ -1,115 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -#if canImport(UIKit) -import UIKit -#endif - -#if canImport(AppKit) -import AppKit -#endif - -/// A namespace for all processors that implement ``ImageProcessing`` protocol. -public enum ImageProcessors {} - -extension ImageProcessing where Self == ImageProcessors.Resize { - /// Scales an image to a specified size. - /// - /// - parameters - /// - size: The target size. - /// - unit: Unit of the target size. By default, `.points`. - /// - contentMode: Target content mode. - /// - crop: If `true` will crop the image to match the target size. Does - /// nothing with content mode .aspectFill. `false` by default. - /// - upscale: Upscaling is not allowed by default. - public static func resize(size: CGSize, unit: ImageProcessingOptions.Unit = .points, contentMode: ImageProcessingOptions.ContentMode = .aspectFill, crop: Bool = false, upscale: Bool = false) -> ImageProcessors.Resize { - ImageProcessors.Resize(size: size, unit: unit, contentMode: contentMode, crop: crop, upscale: upscale) - } - - /// Scales an image to the given width preserving aspect ratio. - /// - /// - parameters: - /// - width: The target width. - /// - unit: Unit of the target size. By default, `.points`. - /// - upscale: `false` by default. - public static func resize(width: CGFloat, unit: ImageProcessingOptions.Unit = .points, upscale: Bool = false) -> ImageProcessors.Resize { - ImageProcessors.Resize(width: width, unit: unit, upscale: upscale) - } - - /// Scales an image to the given height preserving aspect ratio. - /// - /// - parameters: - /// - height: The target height. - /// - unit: Unit of the target size. By default, `.points`. - /// - upscale: `false` by default. - public static func resize(height: CGFloat, unit: ImageProcessingOptions.Unit = .points, upscale: Bool = false) -> ImageProcessors.Resize { - ImageProcessors.Resize(height: height, unit: unit, upscale: upscale) - } -} - -extension ImageProcessing where Self == ImageProcessors.Circle { - /// Rounds the corners of an image into a circle. If the image is not a square, - /// crops it to a square first. - /// - /// - parameter border: `nil` by default. - public static func circle(border: ImageProcessingOptions.Border? = nil) -> ImageProcessors.Circle { - ImageProcessors.Circle(border: border) - } -} - -extension ImageProcessing where Self == ImageProcessors.RoundedCorners { - /// Rounds the corners of an image to the specified radius. - /// - /// - parameters: - /// - radius: The radius of the corners. - /// - unit: Unit of the radius. - /// - border: An optional border drawn around the image. - /// - /// - important: In order for the corners to be displayed correctly, the image must exactly match the size - /// of the image view in which it will be displayed. See ``ImageProcessors/Resize`` for more info. - public static func roundedCorners(radius: CGFloat, unit: ImageProcessingOptions.Unit = .points, border: ImageProcessingOptions.Border? = nil) -> ImageProcessors.RoundedCorners { - ImageProcessors.RoundedCorners(radius: radius, unit: unit, border: border) - } -} - -extension ImageProcessing where Self == ImageProcessors.Anonymous { - /// Creates a custom processor with a given closure. - /// - /// - parameters: - /// - id: Uniquely identifies the operation performed by the processor. - /// - closure: A closure that transforms the images. - public static func process(id: String, _ closure: @Sendable @escaping (PlatformImage) -> PlatformImage?) -> ImageProcessors.Anonymous { - ImageProcessors.Anonymous(id: id, closure) - } -} - -#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) - -extension ImageProcessing where Self == ImageProcessors.CoreImageFilter { - /// Applies Core Image filter – `CIFilter` – to the image. - /// - /// - parameter identifier: Uniquely identifies the processor. - public static func coreImageFilter(name: String, parameters: [String: Any], identifier: String) -> ImageProcessors.CoreImageFilter { - ImageProcessors.CoreImageFilter(name: name, parameters: parameters, identifier: identifier) - } - - /// Applies Core Image filter – `CIFilter` – to the image. - /// - public static func coreImageFilter(name: String) -> ImageProcessors.CoreImageFilter { - ImageProcessors.CoreImageFilter(name: name) - } -} - -extension ImageProcessing where Self == ImageProcessors.GaussianBlur { - /// Blurs an image using `CIGaussianBlur` filter. - /// - /// - parameter radius: `8` by default. - public static func gaussianBlur(radius: Int = 8) -> ImageProcessors.GaussianBlur { - ImageProcessors.GaussianBlur(radius: radius) - } -} - -#endif diff --git a/Nuke/Tasks/AsyncTask.swift b/Nuke/Tasks/AsyncTask.swift deleted file mode 100644 index 5d55151..0000000 --- a/Nuke/Tasks/AsyncTask.swift +++ /dev/null @@ -1,369 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// Represents a task with support for multiple observers, cancellation, -/// progress reporting, dependencies – everything that `ImagePipeline` needs. -/// -/// A `AsyncTask` can have zero or more subscriptions (`TaskSubscription`) which can -/// be used to later unsubscribe or change the priority of the subscription. -/// -/// The task has built-in support for operations (`Foundation.Operation`) – it -/// automatically cancels them, updates the priority, etc. Most steps in the -/// image pipeline are represented using Operation to take advantage of these features. -/// -/// - warning: Must be thread-confined! -class AsyncTask: AsyncTaskSubscriptionDelegate, @unchecked Sendable { - - private struct Subscription { - let closure: (Event) -> Void - weak var subscriber: AnyObject? - var priority: TaskPriority - } - - // In most situations, especially for intermediate tasks, the almost almost - // only one subscription. - private var inlineSubscription: Subscription? - private var subscriptions: [TaskSubscriptionKey: Subscription]? // Create lazily - private var nextSubscriptionKey = 0 - - var subscribers: [AnyObject] { - var output = [AnyObject?]() - output.append(inlineSubscription?.subscriber) - subscriptions?.values.forEach { output.append($0.subscriber) } - return output.compactMap { $0 } - } - - /// Returns `true` if the task was either cancelled, or was completed. - private(set) var isDisposed = false - private var isStarted = false - - /// Gets called when the task is either cancelled, or was completed. - var onDisposed: (() -> Void)? - - var onCancelled: (() -> Void)? - - var priority: TaskPriority = .normal { - didSet { - guard oldValue != priority else { return } - operation?.queuePriority = priority.queuePriority - dependency?.setPriority(priority) - dependency2?.setPriority(priority) - } - } - - /// A task might have a dependency. The task automatically unsubscribes - /// from the dependency when it gets cancelled, and also updates the - /// priority of the subscription to the dependency when its own - /// priority is updated. - var dependency: TaskSubscription? { - didSet { - dependency?.setPriority(priority) - } - } - - // The tasks only ever need up to 2 dependencies and this code is much faster - // than creating an array. - var dependency2: TaskSubscription? { - didSet { - dependency2?.setPriority(priority) - } - } - - weak var operation: Foundation.Operation? { - didSet { - guard priority != .normal else { return } - operation?.queuePriority = priority.queuePriority - } - } - - /// Publishes the results of the task. - var publisher: Publisher { Publisher(task: self) } - - /// Override this to start image task. Only gets called once. - func start() {} - - // MARK: - Managing Observers - - /// - notes: Returns `nil` if the task was disposed. - private func subscribe(priority: TaskPriority = .normal, subscriber: AnyObject? = nil, _ closure: @escaping (Event) -> Void) -> TaskSubscription? { - guard !isDisposed else { return nil } - - let subscriptionKey = nextSubscriptionKey - nextSubscriptionKey += 1 - let subscription = TaskSubscription(task: self, key: subscriptionKey) - - if subscriptionKey == 0 { - inlineSubscription = Subscription(closure: closure, subscriber: subscriber, priority: priority) - } else { - if subscriptions == nil { subscriptions = [:] } - subscriptions![subscriptionKey] = Subscription(closure: closure, subscriber: subscriber, priority: priority) - } - - updatePriority(suggestedPriority: priority) - - if !isStarted { - isStarted = true - start() - } - - // The task may have been completed synchronously by `starter`. - guard !isDisposed else { return nil } - - return subscription - } - - // MARK: - TaskSubscriptionDelegate - - fileprivate func setPriority(_ priority: TaskPriority, for key: TaskSubscriptionKey) { - guard !isDisposed else { return } - - if key == 0 { - inlineSubscription?.priority = priority - } else { - subscriptions![key]?.priority = priority - } - updatePriority(suggestedPriority: priority) - } - - fileprivate func unsubsribe(key: TaskSubscriptionKey) { - if key == 0 { - guard inlineSubscription != nil else { return } - inlineSubscription = nil - } else { - guard subscriptions!.removeValue(forKey: key) != nil else { return } - } - - guard !isDisposed else { return } - - if inlineSubscription == nil && subscriptions?.isEmpty ?? true { - terminate(reason: .cancelled) - } else { - updatePriority(suggestedPriority: nil) - } - } - - // MARK: - Sending Events - - func send(value: Value, isCompleted: Bool = false) { - send(event: .value(value, isCompleted: isCompleted)) - } - - func send(error: Error) { - send(event: .error(error)) - } - - func send(progress: TaskProgress) { - send(event: .progress(progress)) - } - - private func send(event: Event) { - guard !isDisposed else { return } - - switch event { - case let .value(_, isCompleted): - if isCompleted { - terminate(reason: .finished) - } - case .progress: - break // Simply send the event - case .error: - terminate(reason: .finished) - } - - inlineSubscription?.closure(event) - if let subscriptions { - for subscription in subscriptions.values { - subscription.closure(event) - } - } - } - - // MARK: - Termination - - private enum TerminationReason { - case finished, cancelled - } - - private func terminate(reason: TerminationReason) { - guard !isDisposed else { return } - isDisposed = true - - if reason == .cancelled { - operation?.cancel() - dependency?.unsubscribe() - dependency2?.unsubscribe() - onCancelled?() - } - onDisposed?() - } - - // MARK: - Priority - - private func updatePriority(suggestedPriority: TaskPriority?) { - if let suggestedPriority, suggestedPriority >= priority { - // No need to recompute, won't go higher than that - priority = suggestedPriority - return - } - - var newPriority = inlineSubscription?.priority - // Same as subscriptions.map { $0?.priority }.max() but without allocating - // any memory for redundant arrays - if let subscriptions { - for subscription in subscriptions.values { - if newPriority == nil { - newPriority = subscription.priority - } else if subscription.priority > newPriority! { - newPriority = subscription.priority - } - } - } - self.priority = newPriority ?? .normal - } -} - -// MARK: - AsyncTask (Publisher) - -extension AsyncTask { - /// Publishes the results of the task. - struct Publisher { - fileprivate let task: AsyncTask - - /// Attaches the subscriber to the task. - /// - notes: Returns `nil` if the task is already disposed. - func subscribe(priority: TaskPriority = .normal, subscriber: AnyObject? = nil, _ closure: @escaping (Event) -> Void) -> TaskSubscription? { - task.subscribe(priority: priority, subscriber: subscriber, closure) - } - - /// Attaches the subscriber to the task. Automatically forwards progress - /// and error events to the given task. - /// - notes: Returns `nil` if the task is already disposed. - func subscribe(_ task: AsyncTask, onValue: @escaping (Value, Bool) -> Void) -> TaskSubscription? { - subscribe(subscriber: task) { [weak task] event in - guard let task else { return } - switch event { - case let .value(value, isCompleted): - onValue(value, isCompleted) - case let .progress(progress): - task.send(progress: progress) - case let .error(error): - task.send(error: error) - } - } - } - } -} - -typealias TaskProgress = ImageTask.Progress // Using typealias for simplicity - -enum TaskPriority: Int, Comparable { - case veryLow = 0, low, normal, high, veryHigh - - var queuePriority: Operation.QueuePriority { - switch self { - case .veryLow: return .veryLow - case .low: return .low - case .normal: return .normal - case .high: return .high - case .veryHigh: return .veryHigh - } - } - - static func < (lhs: TaskPriority, rhs: TaskPriority) -> Bool { - lhs.rawValue < rhs.rawValue - } -} - -// MARK: - AsyncTask.Event { -extension AsyncTask { - enum Event { - case value(Value, isCompleted: Bool) - case progress(TaskProgress) - case error(Error) - - var isCompleted: Bool { - switch self { - case let .value(_, isCompleted): return isCompleted - case .progress: return false - case .error: return true - } - } - } -} - -extension AsyncTask.Event: Equatable where Value: Equatable, Error: Equatable {} - -// MARK: - TaskSubscription - -/// Represents a subscription to a task. The observer must retain a strong -/// reference to a subscription. -struct TaskSubscription: Sendable { - private let task: any AsyncTaskSubscriptionDelegate - private let key: TaskSubscriptionKey - - fileprivate init(task: any AsyncTaskSubscriptionDelegate, key: TaskSubscriptionKey) { - self.task = task - self.key = key - } - - /// Removes the subscription from the task. The observer won't receive any - /// more events from the task. - /// - /// If there are no more subscriptions attached to the task, the task gets - /// cancelled along with its dependencies. The cancelled task is - /// marked as disposed. - func unsubscribe() { - task.unsubsribe(key: key) - } - - /// Updates the priority of the subscription. The priority of the task is - /// calculated as the maximum priority out of all of its subscription. When - /// the priority of the task is updated, the priority of a dependency also is. - /// - /// - note: The priority also automatically gets updated when the subscription - /// is removed from the task. - func setPriority(_ priority: TaskPriority) { - task.setPriority(priority, for: key) - } -} - -private protocol AsyncTaskSubscriptionDelegate: AnyObject, Sendable { - func unsubsribe(key: TaskSubscriptionKey) - func setPriority(_ priority: TaskPriority, for observer: TaskSubscriptionKey) -} - -private typealias TaskSubscriptionKey = Int - -// MARK: - TaskPool - -/// Contains the tasks which haven't completed yet. -final class TaskPool { - private let isCoalescingEnabled: Bool - private var map = [Key: AsyncTask]() - - init(_ isCoalescingEnabled: Bool) { - self.isCoalescingEnabled = isCoalescingEnabled - } - - /// Creates a task with the given key. If there is an outstanding task with - /// the given key in the pool, the existing task is returned. Tasks are - /// automatically removed from the pool when they are disposed. - func publisherForKey(_ key: @autoclosure () -> Key, _ make: () -> AsyncTask) -> AsyncTask.Publisher { - guard isCoalescingEnabled else { - return make().publisher - } - let key = key() - if let task = map[key] { - return task.publisher - } - let task = make() - map[key] = task - task.onDisposed = { [weak self] in - self?.map[key] = nil - } - return task.publisher - } -} diff --git a/Nuke/Tasks/ImagePipelineTask.swift b/Nuke/Tasks/ImagePipelineTask.swift deleted file mode 100644 index 2068a28..0000000 --- a/Nuke/Tasks/ImagePipelineTask.swift +++ /dev/null @@ -1,38 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -// Each task holds a strong reference to the pipeline. This is by design. The -// user does not need to hold a strong reference to the pipeline. -class ImagePipelineTask: AsyncTask { - let pipeline: ImagePipeline - // A canonical request representing the unit work performed by the task. - let request: ImageRequest - - init(_ pipeline: ImagePipeline, _ request: ImageRequest) { - self.pipeline = pipeline - self.request = request - } -} - -// Returns all image tasks subscribed to the current pipeline task. -// A suboptimal approach just to make the new DiskCachPolicy.automatic work. -protocol ImageTaskSubscribers { - var imageTasks: [ImageTask] { get } -} - -extension ImageTask: ImageTaskSubscribers { - var imageTasks: [ImageTask] { - [self] - } -} - -extension ImagePipelineTask: ImageTaskSubscribers { - var imageTasks: [ImageTask] { - subscribers.flatMap { subscribers -> [ImageTask] in - (subscribers as? ImageTaskSubscribers)?.imageTasks ?? [] - } - } -} diff --git a/Nuke/Tasks/OperationTask.swift b/Nuke/Tasks/OperationTask.swift deleted file mode 100644 index fd3da53..0000000 --- a/Nuke/Tasks/OperationTask.swift +++ /dev/null @@ -1,35 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// A one-shot task for performing a single () -> T function. -final class OperationTask: AsyncTask { - private let pipeline: ImagePipeline - private let queue: OperationQueue - private let process: () throws -> T - - init(_ pipeline: ImagePipeline, _ queue: OperationQueue, _ process: @escaping () throws -> T) { - self.pipeline = pipeline - self.queue = queue - self.process = process - } - - override func start() { - operation = queue.add { [weak self] in - guard let self else { return } - let result = Result(catching: { try self.process() }) - self.pipeline.queue.async { - switch result { - case .success(let value): - self.send(value: value, isCompleted: true) - case .failure(let error): - self.send(error: error) - } - } - } - } - - struct Error: Swift.Error {} -} diff --git a/Nuke/Tasks/TaskFetchDecodedImage.swift b/Nuke/Tasks/TaskFetchDecodedImage.swift deleted file mode 100644 index 04f4fec..0000000 --- a/Nuke/Tasks/TaskFetchDecodedImage.swift +++ /dev/null @@ -1,84 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// Receives data from ``TaskLoadImageData`` and decodes it as it arrives. -final class TaskFetchDecodedImage: ImagePipelineTask { - private var decoder: (any ImageDecoding)? - - override func start() { - dependency = pipeline.makeTaskFetchOriginalImageData(for: request).subscribe(self) { [weak self] in - self?.didReceiveData($0.0, urlResponse: $0.1, isCompleted: $1) - } - } - - /// Receiving data from `OriginalDataTask`. - private func didReceiveData(_ data: Data, urlResponse: URLResponse?, isCompleted: Bool) { - guard isCompleted || pipeline.configuration.isProgressiveDecodingEnabled else { - return - } - - if !isCompleted && operation != nil { - return // Back pressure - already decoding another progressive data chunk - } - - if isCompleted { - operation?.cancel() // Cancel any potential pending progressive decoding tasks - } - - let context = ImageDecodingContext(request: request, data: data, isCompleted: isCompleted, urlResponse: urlResponse, cacheType: nil) - guard let decoder = getDecoder(for: context) else { - if isCompleted { - send(error: .decoderNotRegistered(context: context)) - } else { - // Try again when more data is downloaded. - } - return - } - - // Fast-track default decoders, most work is already done during - // initialization anyway. - @Sendable func decode() -> Result { - signpost("DecodeImageData", isCompleted ? "FinalImage" : "ProgressiveImage") { - Result(catching: { try decoder.decode(context) }) - } - } - - if !decoder.isAsynchronous { - didFinishDecoding(decoder: decoder, context: context, result: decode()) - } else { - operation = pipeline.configuration.imageDecodingQueue.add { [weak self] in - guard let self else { return } - - let result = decode() - self.pipeline.queue.async { - self.didFinishDecoding(decoder: decoder, context: context, result: result) - } - } - } - } - - private func didFinishDecoding(decoder: any ImageDecoding, context: ImageDecodingContext, result: Result) { - switch result { - case .success(let response): - send(value: response, isCompleted: context.isCompleted) - case .failure(let error): - if context.isCompleted { - send(error: .decodingFailed(decoder: decoder, context: context, error: error)) - } - } - } - - // Lazily creates decoding for task - private func getDecoder(for context: ImageDecodingContext) -> (any ImageDecoding)? { - // Return the existing processor in case it has already been created. - if let decoder { - return decoder - } - let decoder = pipeline.delegate.imageDecoder(for: context, pipeline: pipeline) - self.decoder = decoder - return decoder - } -} diff --git a/Nuke/Tasks/TaskFetchOriginalImageData.swift b/Nuke/Tasks/TaskFetchOriginalImageData.swift deleted file mode 100644 index c4e1dc3..0000000 --- a/Nuke/Tasks/TaskFetchOriginalImageData.swift +++ /dev/null @@ -1,191 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// Fetches original image from the data loader (`DataLoading`) and stores it -/// in the disk cache (`DataCaching`). -final class TaskFetchOriginalImageData: ImagePipelineTask<(Data, URLResponse?)> { - private var urlResponse: URLResponse? - private var resumableData: ResumableData? - private var resumedDataCount: Int64 = 0 - private var data = Data() - - override func start() { - guard let urlRequest = request.urlRequest else { - // A malformed URL prevented a URL request from being initiated. - send(error: .dataLoadingFailed(error: URLError(.badURL))) - return - } - - if let rateLimiter = pipeline.rateLimiter { - // Rate limiter is synchronized on pipeline's queue. Delayed work is - // executed asynchronously also on the same queue. - rateLimiter.execute { [weak self] in - guard let self, !self.isDisposed else { - return false - } - self.loadData(urlRequest: urlRequest) - return true - } - } else { // Start loading immediately. - loadData(urlRequest: urlRequest) - } - } - - private func loadData(urlRequest: URLRequest) { - if request.options.contains(.skipDataLoadingQueue) { - loadData(urlRequest: urlRequest, finish: { /* do nothing */ }) - } else { - // Wrap data request in an operation to limit the maximum number of - // concurrent data tasks. - operation = pipeline.configuration.dataLoadingQueue.add { [weak self] finish in - guard let self else { - return finish() - } - self.pipeline.queue.async { - self.loadData(urlRequest: urlRequest, finish: finish) - } - } - } - } - - // This methods gets called inside data loading operation (Operation). - private func loadData(urlRequest: URLRequest, finish: @escaping () -> Void) { - guard !isDisposed else { - return finish() - } - // Read and remove resumable data from cache (we're going to insert it - // back in the cache if the request fails to complete again). - var urlRequest = urlRequest - if pipeline.configuration.isResumableDataEnabled, - let resumableData = ResumableDataStorage.shared.removeResumableData(for: request, pipeline: pipeline) { - // Update headers to add "Range" and "If-Range" headers - resumableData.resume(request: &urlRequest) - // Save resumable data to be used later (before using it, the pipeline - // verifies that the server returns "206 Partial Content") - self.resumableData = resumableData - } - - signpost(self, "LoadImageData", .begin, "URL: \(urlRequest.url?.absoluteString ?? ""), resumable data: \(Formatter.bytes(resumableData?.data.count ?? 0))") - - let dataLoader = pipeline.delegate.dataLoader(for: request, pipeline: pipeline) - let dataTask = dataLoader.loadData(with: urlRequest, didReceiveData: { [weak self] data, response in - guard let self else { return } - self.pipeline.queue.async { - self.dataTask(didReceiveData: data, response: response) - } - }, completion: { [weak self] error in - finish() // Finish the operation! - guard let self else { return } - signpost(self, "LoadImageData", .end, "Finished with size \(Formatter.bytes(self.data.count))") - self.pipeline.queue.async { - self.dataTaskDidFinish(error: error) - } - }) - - onCancelled = { [weak self] in - guard let self else { return } - - signpost(self, "LoadImageData", .end, "Cancelled") - dataTask.cancel() - finish() // Finish the operation! - - self.tryToSaveResumableData() - } - } - - private func dataTask(didReceiveData chunk: Data, response: URLResponse) { - // Check if this is the first response. - if urlResponse == nil { - // See if the server confirmed that the resumable data can be used - if let resumableData, ResumableData.isResumedResponse(response) { - data = resumableData.data - resumedDataCount = Int64(resumableData.data.count) - signpost(self, "LoadImageData", .event, "Resumed with data \(Formatter.bytes(resumedDataCount))") - } - resumableData = nil // Get rid of resumable data - } - - // Append data and save response - if data.isEmpty { - data = chunk - } else { - data.append(chunk) - } - urlResponse = response - - let progress = TaskProgress(completed: Int64(data.count), total: response.expectedContentLength + resumedDataCount) - send(progress: progress) - - // If the image hasn't been fully loaded yet, give decoder a change - // to decode the data chunk. In case `expectedContentLength` is `0`, - // progressive decoding doesn't run. - guard data.count < response.expectedContentLength else { return } - - send(value: (data, response)) - } - - private func dataTaskDidFinish(error: Swift.Error?) { - if let error { - tryToSaveResumableData() - send(error: .dataLoadingFailed(error: error)) - return - } - - // Sanity check, should never happen in practice - guard !data.isEmpty else { - send(error: .dataIsEmpty) - return - } - - // Store in data cache - storeDataInCacheIfNeeded(data) - - send(value: (data, urlResponse), isCompleted: true) - } - - private func tryToSaveResumableData() { - // Try to save resumable data in case the task was cancelled - // (`URLError.cancelled`) or failed to complete with other error. - if pipeline.configuration.isResumableDataEnabled, - let response = urlResponse, !data.isEmpty, - let resumableData = ResumableData(response: response, data: data) { - ResumableDataStorage.shared.storeResumableData(resumableData, for: request, pipeline: pipeline) - } - } -} - -extension ImagePipelineTask where Value == (Data, URLResponse?) { - func storeDataInCacheIfNeeded(_ data: Data) { - guard let dataCache = pipeline.delegate.dataCache(for: request, pipeline: pipeline), shouldStoreDataInDiskCache() else { - return - } - let key = pipeline.cache.makeDataCacheKey(for: request) - pipeline.delegate.willCache(data: data, image: nil, for: request, pipeline: pipeline) { - guard let data = $0 else { return } - // Important! Storing directly ignoring `ImageRequest.Options`. - dataCache.storeData(data, for: key) - } - } - - private func shouldStoreDataInDiskCache() -> Bool { - guard imageTasks.contains(where: { !$0.request.options.contains(.disableDiskCacheWrites) }) else { - return false - } - guard !(request.url?.isLocalResource ?? false) else { - return false - } - switch pipeline.configuration.dataCachePolicy { - case .automatic: - return imageTasks.contains { $0.request.processors.isEmpty } - case .storeOriginalData: - return true - case .storeEncodedImages: - return false - case .storeAll: - return true - } - } -} diff --git a/Nuke/Tasks/TaskFetchWithPublisher.swift b/Nuke/Tasks/TaskFetchWithPublisher.swift deleted file mode 100644 index 713b15d..0000000 --- a/Nuke/Tasks/TaskFetchWithPublisher.swift +++ /dev/null @@ -1,72 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// Fetches data using the publisher provided with the request. -/// Unlike `TaskFetchOriginalImageData`, there is no resumable data involved. -final class TaskFetchWithPublisher: ImagePipelineTask<(Data, URLResponse?)> { - private lazy var data = Data() - - override func start() { - if request.options.contains(.skipDataLoadingQueue) { - loadData(finish: { /* do nothing */ }) - } else { - // Wrap data request in an operation to limit the maximum number of - // concurrent data tasks. - operation = pipeline.configuration.dataLoadingQueue.add { [weak self] finish in - guard let self else { - return finish() - } - self.pipeline.queue.async { - self.loadData { finish() } - } - } - } - } - - // This methods gets called inside data loading operation (Operation). - private func loadData(finish: @escaping () -> Void) { - guard !isDisposed else { - return finish() - } - - guard let publisher = request.publisher else { - send(error: .dataLoadingFailed(error: URLError(.unknown))) // This is just a placeholder error, never thrown - return assertionFailure("This should never happen") - } - - let cancellable = publisher.sink(receiveCompletion: { [weak self] result in - finish() // Finish the operation! - guard let self else { return } - self.pipeline.queue.async { - self.dataTaskDidFinish(result) - } - }, receiveValue: { [weak self] data in - guard let self else { return } - self.pipeline.queue.async { - self.data.append(data) - } - }) - - onCancelled = { - finish() - cancellable.cancel() - } - } - - private func dataTaskDidFinish(_ result: PublisherCompletion) { - switch result { - case .finished: - guard !data.isEmpty else { - send(error: .dataIsEmpty) - return - } - storeDataInCacheIfNeeded(data) - send(value: (data, nil), isCompleted: true) - case .failure(let error): - send(error: .dataLoadingFailed(error: error)) - } - } -} diff --git a/Nuke/Tasks/TaskLoadData.swift b/Nuke/Tasks/TaskLoadData.swift deleted file mode 100644 index e7e113c..0000000 --- a/Nuke/Tasks/TaskLoadData.swift +++ /dev/null @@ -1,47 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// Wrapper for tasks created by `loadData` calls. -final class TaskLoadData: ImagePipelineTask<(Data, URLResponse?)> { - override func start() { - guard let dataCache = pipeline.delegate.dataCache(for: request, pipeline: pipeline), - !request.options.contains(.disableDiskCacheReads) else { - loadData() - return - } - operation = pipeline.configuration.dataCachingQueue.add { [weak self] in - self?.getCachedData(dataCache: dataCache) - } - } - - private func getCachedData(dataCache: any DataCaching) { - let data = signpost("ReadCachedImageData") { - pipeline.cache.cachedData(for: request) - } - pipeline.queue.async { - if let data { - self.send(value: (data, nil), isCompleted: true) - } else { - self.loadData() - } - } - } - - private func loadData() { - guard !request.options.contains(.returnCacheDataDontLoad) else { - return send(error: .dataMissingInCache) - } - - let request = self.request.withProcessors([]) - dependency = pipeline.makeTaskFetchOriginalImageData(for: request).subscribe(self) { [weak self] in - self?.didReceiveData($0.0, urlResponse: $0.1, isCompleted: $1) - } - } - - private func didReceiveData(_ data: Data, urlResponse: URLResponse?, isCompleted: Bool) { - send(value: (data, urlResponse), isCompleted: isCompleted) - } -} diff --git a/Nuke/Tasks/TaskLoadImage.swift b/Nuke/Tasks/TaskLoadImage.swift deleted file mode 100644 index 56fc61d..0000000 --- a/Nuke/Tasks/TaskLoadImage.swift +++ /dev/null @@ -1,275 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -/// Wrapper for tasks created by `loadImage` calls. -/// -/// Performs all the quick cache lookups and also manages image processing. -/// The coalescing for image processing is implemented on demand (extends the -/// scenarios in which coalescing can kick in). -final class TaskLoadImage: ImagePipelineTask { - override func start() { - // Memory cache lookup - if let image = pipeline.cache[request] { - let response = ImageResponse(container: image, request: request, cacheType: .memory) - send(value: response, isCompleted: !image.isPreview) - if !image.isPreview { - return // Already got the result! - } - } - - // Disk cache lookup - if let dataCache = pipeline.delegate.dataCache(for: request, pipeline: pipeline), - !request.options.contains(.disableDiskCacheReads) { - operation = pipeline.configuration.dataCachingQueue.add { [weak self] in - self?.getCachedData(dataCache: dataCache) - } - return - } - - // Fetch image - fetchImage() - } - - // MARK: Disk Cache Lookup - - private func getCachedData(dataCache: any DataCaching) { - let data = signpost("ReadCachedProcessedImageData") { - pipeline.cache.cachedData(for: request) - } - pipeline.queue.async { - if let data { - self.didReceiveCachedData(data) - } else { - self.fetchImage() - } - } - } - - private func didReceiveCachedData(_ data: Data) { - guard !isDisposed else { return } - - let context = ImageDecodingContext(request: request, data: data, isCompleted: true, urlResponse: nil, cacheType: .disk) - guard let decoder = pipeline.delegate.imageDecoder(for: context, pipeline: pipeline) else { - // This shouldn't happen in practice unless encoder/decoder pair - // for data cache is misconfigured. - return fetchImage() - } - - @Sendable func decode() -> ImageResponse? { - signpost("DecodeCachedProcessedImageData") { - try? decoder.decode(context) - } - } - if !decoder.isAsynchronous { - didDecodeCachedData(decode()) - } else { - operation = pipeline.configuration.imageDecodingQueue.add { [weak self] in - guard let self else { return } - let response = decode() - self.pipeline.queue.async { - self.didDecodeCachedData(response) - } - } - } - } - - private func didDecodeCachedData(_ response: ImageResponse?) { - if let response { - decompressImage(response, isCompleted: true, isFromDiskCache: true) - } else { - fetchImage() - } - } - - // MARK: Fetch Image - - private func fetchImage() { - // Memory cache lookup for intermediate images. - // For example, for processors ["p1", "p2"], check only ["p1"]. - // Then apply the remaining processors. - // - // We are not performing data cache lookup for intermediate requests - // for now (because it's not free), but maybe adding an option would be worth it. - // You can emulate this behavior by manually creating intermediate requests. - if request.processors.count > 1 { - var processors = request.processors - var remaining: [any ImageProcessing] = [] - if let last = processors.popLast() { - remaining.append(last) - } - while !processors.isEmpty { - if let image = pipeline.cache[request.withProcessors(processors)] { - let response = ImageResponse(container: image, request: request, cacheType: .memory) - process(response, isCompleted: !image.isPreview, processors: remaining) - if !image.isPreview { - return // Nothing left to do, just apply the processors - } else { - break - } - } - if let last = processors.popLast() { - remaining.append(last) - } - } - } - - let processors: [any ImageProcessing] = request.processors.reversed() - // The only remaining choice is to fetch the image - if request.options.contains(.returnCacheDataDontLoad) { - send(error: .dataMissingInCache) - } else if request.processors.isEmpty { - dependency = pipeline.makeTaskFetchDecodedImage(for: request).subscribe(self) { [weak self] in - self?.process($0, isCompleted: $1, processors: processors) - } - } else { - let request = self.request.withProcessors([]) - dependency = pipeline.makeTaskLoadImage(for: request).subscribe(self) { [weak self] in - self?.process($0, isCompleted: $1, processors: processors) - } - } - } - - // MARK: Processing - - /// - parameter processors: Remaining processors to by applied - private func process(_ response: ImageResponse, isCompleted: Bool, processors: [any ImageProcessing]) { - if isCompleted { - dependency2?.unsubscribe() // Cancel any potential pending progressive processing tasks - } else if dependency2 != nil { - return // Back pressure - already processing another progressive image - } - - _process(response, isCompleted: isCompleted, processors: processors) - } - - /// - parameter processors: Remaining processors to by applied - private func _process(_ response: ImageResponse, isCompleted: Bool, processors: [any ImageProcessing]) { - guard let processor = processors.last else { - self.decompressImage(response, isCompleted: isCompleted) - return - } - - let key = ImageProcessingKey(image: response, processor: processor) - let context = ImageProcessingContext(request: request, response: response, isCompleted: isCompleted) - dependency2 = pipeline.makeTaskProcessImage(key: key, process: { - try signpost("ProcessImage", isCompleted ? "FinalImage" : "ProgressiveImage") { - var response = response - response.container = try processor.process(response.container, context: context) - return response - } - }).subscribe(priority: priority) { [weak self] event in - guard let self else { return } - if event.isCompleted { - self.dependency2 = nil - } - switch event { - case .value(let response, _): - self._process(response, isCompleted: isCompleted, processors: processors.dropLast()) - case .error(let error): - if isCompleted { - self.send(error: .processingFailed(processor: processor, context: context, error: error)) - } - case .progress: - break // Do nothing (Not reported by OperationTask) - } - } - } - - // MARK: Decompression - - private func decompressImage(_ response: ImageResponse, isCompleted: Bool, isFromDiskCache: Bool = false) { - guard isDecompressionNeeded(for: response) else { - storeImageInCaches(response, isFromDiskCache: isFromDiskCache) - send(value: response, isCompleted: isCompleted) - return - } - - if isCompleted { - operation?.cancel() // Cancel any potential pending progressive decompression tasks - } else if operation != nil { - return // Back-pressure: we are receiving data too fast - } - - guard !isDisposed else { return } - - operation = pipeline.configuration.imageDecompressingQueue.add { [weak self] in - guard let self else { return } - - let response = signpost("DecompressImage", isCompleted ? "FinalImage" : "ProgressiveImage") { - self.pipeline.delegate.decompress(response: response, request: self.request, pipeline: self.pipeline) - } - - self.pipeline.queue.async { - self.storeImageInCaches(response, isFromDiskCache: isFromDiskCache) - self.send(value: response, isCompleted: isCompleted) - } - } - } - - private func isDecompressionNeeded(for response: ImageResponse) -> Bool { - (ImageDecompression.isDecompressionNeeded(for: response.image) ?? false) && - !request.options.contains(.skipDecompression) && - pipeline.delegate.shouldDecompress(response: response, for: request, pipeline: pipeline) - } - - // MARK: Caching - - private func storeImageInCaches(_ response: ImageResponse, isFromDiskCache: Bool) { - guard subscribers.contains(where: { $0 is ImageTask }) else { - return // Only store for direct requests - } - // Memory cache (ImageCaching) - pipeline.cache[request] = response.container - // Disk cache (DataCaching) - if !isFromDiskCache { - storeImageInDataCache(response) - } - } - - private func storeImageInDataCache(_ response: ImageResponse) { - guard !response.container.isPreview else { - return - } - guard let dataCache = pipeline.delegate.dataCache(for: request, pipeline: pipeline), shouldStoreImageInDiskCache() else { - return - } - let context = ImageEncodingContext(request: request, image: response.image, urlResponse: response.urlResponse) - let encoder = pipeline.delegate.imageEncoder(for: context, pipeline: pipeline) - let key = pipeline.cache.makeDataCacheKey(for: request) - pipeline.configuration.imageEncodingQueue.addOperation { [weak pipeline, request] in - guard let pipeline else { return } - let encodedData = signpost("EncodeImage") { - encoder.encode(response.container, context: context) - } - guard let data = encodedData else { return } - pipeline.delegate.willCache(data: data, image: response.container, for: request, pipeline: pipeline) { - guard let data = $0 else { return } - // Important! Storing directly ignoring `ImageRequest.Options`. - dataCache.storeData(data, for: key) // This is instant, writes are async - } - } - if pipeline.configuration.debugIsSyncImageEncoding { // Only for debug - pipeline.configuration.imageEncodingQueue.waitUntilAllOperationsAreFinished() - } - } - - private func shouldStoreImageInDiskCache() -> Bool { - guard !(request.url?.isLocalResource ?? false) else { - return false - } - let isProcessed = !request.processors.isEmpty - switch pipeline.configuration.dataCachePolicy { - case .automatic: - return isProcessed - case .storeOriginalData: - return false - case .storeEncodedImages: - return isProcessed || imageTasks.contains { $0.request.processors.isEmpty } - case .storeAll: - return isProcessed - } - } -} diff --git a/NukeExtensions/ImageLoadingOptions.swift b/NukeExtensions/ImageLoadingOptions.swift deleted file mode 100644 index 9e3b0b0..0000000 --- a/NukeExtensions/ImageLoadingOptions.swift +++ /dev/null @@ -1,227 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -#if !os(macOS) -import UIKit.UIImage -import UIKit.UIColor -#else -import AppKit.NSImage -#endif - -/// A set of options that control how the image is loaded and displayed. -public struct ImageLoadingOptions { - /// Shared options. - public static var shared = ImageLoadingOptions() - - /// Placeholder to be displayed when the image is loading. `nil` by default. - public var placeholder: PlatformImage? - - /// Image to be displayed when the request fails. `nil` by default. - public var failureImage: PlatformImage? - -#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) - - /// The image transition animation performed when displaying a loaded image. - /// Only runs when the image was not found in memory cache. `nil` by default. - public var transition: Transition? - - /// The image transition animation performed when displaying a failure image. - /// `nil` by default. - public var failureImageTransition: Transition? - - /// If true, the requested image will always appear with transition, even - /// when loaded from cache. - public var alwaysTransition = false - - func transition(for response: ResponseType) -> Transition? { - switch response { - case .success: return transition - case .failure: return failureImageTransition - case .placeholder: return nil - } - } - -#endif - - /// If true, every time you request a new image for a view, the view will be - /// automatically prepared for reuse: image will be set to `nil`, and animations - /// will be removed. `true` by default. - public var isPrepareForReuseEnabled = true - - /// If `true`, every progressively generated preview produced by the pipeline - /// is going to be displayed. `true` by default. - /// - /// - note: To enable progressive decoding, see `ImagePipeline.Configuration`, - /// `isProgressiveDecodingEnabled` option. - public var isProgressiveRenderingEnabled = true - - /// Custom pipeline to be used. `nil` by default. - public var pipeline: ImagePipeline? - - /// Image processors to be applied unless the processors are provided in the - /// request. `[]` by default. - public var processors: [any ImageProcessing] = [] - -#if os(iOS) || os(tvOS) || os(visionOS) - - /// Content modes to be used for each image type (placeholder, success, - /// failure). `nil` by default (don't change content mode). - public var contentModes: ContentModes? - - /// Custom content modes to be used for each image type (placeholder, success, - /// failure). - public struct ContentModes { - /// Content mode to be used for the loaded image. - public var success: UIView.ContentMode - /// Content mode to be used when displaying a `failureImage`. - public var failure: UIView.ContentMode - /// Content mode to be used when displaying a `placeholder`. - public var placeholder: UIView.ContentMode - - /// - parameters: - /// - success: A content mode to be used with a loaded image. - /// - failure: A content mode to be used with a `failureImage`. - /// - placeholder: A content mode to be used with a `placeholder`. - public init(success: UIView.ContentMode, failure: UIView.ContentMode, placeholder: UIView.ContentMode) { - self.success = success; self.failure = failure; self.placeholder = placeholder - } - } - - func contentMode(for response: ResponseType) -> UIView.ContentMode? { - switch response { - case .success: return contentModes?.success - case .placeholder: return contentModes?.placeholder - case .failure: return contentModes?.failure - } - } - - /// Tint colors to be used for each image type (placeholder, success, - /// failure). `nil` by default (don't change tint color or rendering mode). - public var tintColors: TintColors? - - /// Custom tint color to be used for each image type (placeholder, success, - /// failure). - public struct TintColors { - /// Tint color to be used for the loaded image. - public var success: UIColor? - /// Tint color to be used when displaying a `failureImage`. - public var failure: UIColor? - /// Tint color to be used when displaying a `placeholder`. - public var placeholder: UIColor? - - /// - parameters: - /// - success: A tint color to be used with a loaded image. - /// - failure: A tint color to be used with a `failureImage`. - /// - placeholder: A tint color to be used with a `placeholder`. - public init(success: UIColor?, failure: UIColor?, placeholder: UIColor?) { - self.success = success; self.failure = failure; self.placeholder = placeholder - } - } - - func tintColor(for response: ResponseType) -> UIColor? { - switch response { - case .success: return tintColors?.success - case .placeholder: return tintColors?.placeholder - case .failure: return tintColors?.failure - } - } - -#endif - -#if os(iOS) || os(tvOS) || os(visionOS) - - /// - parameters: - /// - placeholder: Placeholder to be displayed when the image is loading. - /// - transition: The image transition animation performed when - /// displaying a loaded image. Only runs when the image was not found in - /// memory cache. - /// - failureImage: Image to be displayed when request fails. - /// - failureImageTransition: The image transition animation - /// performed when displaying a failure image. - /// - contentModes: Content modes to be used for each image type - /// (placeholder, success, failure). - public init(placeholder: UIImage? = nil, transition: Transition? = nil, failureImage: UIImage? = nil, failureImageTransition: Transition? = nil, contentModes: ContentModes? = nil, tintColors: TintColors? = nil) { - self.placeholder = placeholder - self.transition = transition - self.failureImage = failureImage - self.failureImageTransition = failureImageTransition - self.contentModes = contentModes - self.tintColors = tintColors - } - -#elseif os(macOS) - - public init(placeholder: NSImage? = nil, transition: Transition? = nil, failureImage: NSImage? = nil, failureImageTransition: Transition? = nil) { - self.placeholder = placeholder - self.transition = transition - self.failureImage = failureImage - self.failureImageTransition = failureImageTransition - } - -#elseif os(watchOS) - - public init(placeholder: UIImage? = nil, failureImage: UIImage? = nil) { - self.placeholder = placeholder - self.failureImage = failureImage - } - -#endif - - /// An animated image transition. - public struct Transition { - var style: Style - -#if os(iOS) || os(tvOS) || os(visionOS) - enum Style { // internal representation - case fadeIn(parameters: Parameters) - case custom((ImageDisplayingView, UIImage) -> Void) - } - - struct Parameters { // internal representation - let duration: TimeInterval - let options: UIView.AnimationOptions - } - - /// Fade-in transition (cross-fade in case the image view is already - /// displaying an image). - public static func fadeIn(duration: TimeInterval, options: UIView.AnimationOptions = .allowUserInteraction) -> Transition { - Transition(style: .fadeIn(parameters: Parameters(duration: duration, options: options))) - } - - /// Custom transition. Only runs when the image was not found in memory cache. - public static func custom(_ closure: @escaping (ImageDisplayingView, UIImage) -> Void) -> Transition { - Transition(style: .custom(closure)) - } -#elseif os(macOS) - enum Style { // internal representation - case fadeIn(parameters: Parameters) - case custom((ImageDisplayingView, NSImage) -> Void) - } - - struct Parameters { // internal representation - let duration: TimeInterval - } - - /// Fade-in transition. - public static func fadeIn(duration: TimeInterval) -> Transition { - Transition(style: .fadeIn(parameters: Parameters(duration: duration))) - } - - /// Custom transition. Only runs when the image was not found in memory cache. - public static func custom(_ closure: @escaping (ImageDisplayingView, NSImage) -> Void) -> Transition { - Transition(style: .custom(closure)) - } -#else - enum Style {} -#endif - } - - public init() {} - - enum ResponseType { - case success, failure, placeholder - } -} diff --git a/NukeExtensions/ImageViewExtensions.swift b/NukeExtensions/ImageViewExtensions.swift deleted file mode 100644 index acd3c48..0000000 --- a/NukeExtensions/ImageViewExtensions.swift +++ /dev/null @@ -1,451 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -#if !os(macOS) -import UIKit.UIImage -import UIKit.UIColor -#else -import AppKit.NSImage -#endif - -#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) - -/// Displays images. Add the conformance to this protocol to your views to make -/// them compatible with Nuke image loading extensions. -/// -/// The protocol is defined as `@objc` to make it possible to override its -/// methods in extensions (e.g. you can override `nuke_display(image:data:)` in -/// `UIImageView` subclass like `Gifu.ImageView). -/// -/// The protocol and its methods have prefixes to make sure they don't clash -/// with other similar methods and protocol in Objective-C runtime. -@MainActor -@objc public protocol Nuke_ImageDisplaying { - /// Display a given image. - @objc func nuke_display(image: PlatformImage?, data: Data?) - -#if os(macOS) - @objc var layer: CALayer? { get } -#endif -} - -extension Nuke_ImageDisplaying { - func display(_ container: ImageContainer) { - nuke_display(image: container.image, data: container.data) - } -} - -#if os(macOS) -extension Nuke_ImageDisplaying { - public var layer: CALayer? { nil } -} -#endif - -#if os(iOS) || os(tvOS) || os(visionOS) -import UIKit -/// A `UIView` that implements `ImageDisplaying` protocol. -public typealias ImageDisplayingView = UIView & Nuke_ImageDisplaying - -extension UIImageView: Nuke_ImageDisplaying { - /// Displays an image. - open func nuke_display(image: UIImage?, data: Data? = nil) { - self.image = image - } -} -#elseif os(macOS) -import Cocoa -/// An `NSObject` that implements `ImageDisplaying` and `Animating` protocols. -/// Can support `NSView` and `NSCell`. The latter can return nil for layer. -public typealias ImageDisplayingView = NSObject & Nuke_ImageDisplaying - -extension NSImageView: Nuke_ImageDisplaying { - /// Displays an image. - open func nuke_display(image: NSImage?, data: Data? = nil) { - self.image = image - } -} -#endif - -#if os(tvOS) -import TVUIKit - -extension TVPosterView: Nuke_ImageDisplaying { - /// Displays an image. - open func nuke_display(image: UIImage?, data: Data? = nil) { - self.image = image - } -} -#endif - -// MARK: - ImageView Extensions - -/// Loads an image with the given request and displays it in the view. -/// -/// See the complete method signature for more information. -@MainActor -@discardableResult public func loadImage( - with url: URL?, - options: ImageLoadingOptions = ImageLoadingOptions.shared, - into view: ImageDisplayingView, - completion: @escaping (_ result: Result) -> Void -) -> ImageTask? { - loadImage(with: url, options: options, into: view, progress: nil, completion: completion) -} - -/// Loads an image with the given request and displays it in the view. -/// -/// Before loading a new image, the view is prepared for reuse by canceling any -/// outstanding requests and removing a previously displayed image. -/// -/// If the image is stored in the memory cache, it is displayed immediately with -/// no animations. If not, the image is loaded using an image pipeline. When the -/// image is loading, the `placeholder` is displayed. When the request -/// completes the loaded image is displayed (or `failureImage` in case of an error) -/// with the selected animation. -/// -/// - parameters: -/// - request: The image request. If `nil`, it's handled as a failure scenario. -/// - options: `ImageLoadingOptions.shared` by default. -/// - view: Nuke keeps a weak reference to the view. If the view is deallocated -/// the associated request automatically gets canceled. -/// - progress: A closure to be called periodically on the main thread -/// when the progress is updated. -/// - completion: A closure to be called on the main thread when the -/// request is finished. Gets called synchronously if the response was found in -/// the memory cache. -/// -/// - returns: An image task or `nil` if the image was found in the memory cache. -@MainActor -@discardableResult public func loadImage( - with url: URL?, - options: ImageLoadingOptions = ImageLoadingOptions.shared, - into view: ImageDisplayingView, - progress: ((_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)? = nil, - completion: ((_ result: Result) -> Void)? = nil -) -> ImageTask? { - let controller = ImageViewController.controller(for: view) - return controller.loadImage(with: url.map({ ImageRequest(url: $0) }), options: options, progress: progress, completion: completion) -} - -/// Loads an image with the given request and displays it in the view. -/// -/// See the complete method signature for more information. -@MainActor -@discardableResult public func loadImage( - with request: ImageRequest?, - options: ImageLoadingOptions = ImageLoadingOptions.shared, - into view: ImageDisplayingView, - completion: @escaping (_ result: Result) -> Void -) -> ImageTask? { - loadImage(with: request, options: options, into: view, progress: nil, completion: completion) -} - -/// Loads an image with the given request and displays it in the view. -/// -/// Before loading a new image, the view is prepared for reuse by canceling any -/// outstanding requests and removing a previously displayed image. -/// -/// If the image is stored in the memory cache, it is displayed immediately with -/// no animations. If not, the image is loaded using an image pipeline. When the -/// image is loading, the `placeholder` is displayed. When the request -/// completes the loaded image is displayed (or `failureImage` in case of an error) -/// with the selected animation. -/// -/// - parameters: -/// - request: The image request. If `nil`, it's handled as a failure scenario. -/// - options: `ImageLoadingOptions.shared` by default. -/// - view: Nuke keeps a weak reference to the view. If the view is deallocated -/// the associated request automatically gets canceled. -/// - progress: A closure to be called periodically on the main thread -/// when the progress is updated. -/// - completion: A closure to be called on the main thread when the -/// request is finished. Gets called synchronously if the response was found in -/// the memory cache. -/// -/// - returns: An image task or `nil` if the image was found in the memory cache. -@MainActor -@discardableResult public func loadImage( - with request: ImageRequest?, - options: ImageLoadingOptions = ImageLoadingOptions.shared, - into view: ImageDisplayingView, - progress: ((_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)? = nil, - completion: ((_ result: Result) -> Void)? = nil -) -> ImageTask? { - let controller = ImageViewController.controller(for: view) - return controller.loadImage(with: request, options: options, progress: progress, completion: completion) -} - -/// Cancels an outstanding request associated with the view. -@MainActor -public func cancelRequest(for view: ImageDisplayingView) { - ImageViewController.controller(for: view).cancelOutstandingTask() -} - -// MARK: - ImageViewController - -/// Manages image requests on behalf of an image view. -/// -/// - note: With a few modifications this might become public at some point, -/// however as it stands today `ImageViewController` is just a helper class, -/// making it public wouldn't expose any additional functionality to the users. -@MainActor -private final class ImageViewController { - private weak var imageView: ImageDisplayingView? - private var task: ImageTask? - private var options: ImageLoadingOptions - -#if os(iOS) || os(tvOS) || os(visionOS) - // Image view used for cross-fade transition between images with different - // content modes. - private lazy var transitionImageView = UIImageView() -#endif - - // Automatically cancel the request when the view is deallocated. - deinit { - task?.cancel() - } - - init(view: /* weak */ ImageDisplayingView) { - self.imageView = view - self.options = .shared - } - - // MARK: - Associating Controller - - static var controllerAK: UInt8 = 0 - - // Lazily create a controller for a given view and associate it with a view. - static func controller(for view: ImageDisplayingView) -> ImageViewController { - if let controller = objc_getAssociatedObject(view, &ImageViewController.controllerAK) as? ImageViewController { - return controller - } - let controller = ImageViewController(view: view) - objc_setAssociatedObject(view, &ImageViewController.controllerAK, controller, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - return controller - } - - // MARK: - Loading Images - - func loadImage( - with request: ImageRequest?, - options: ImageLoadingOptions, - progress: ((_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)? = nil, - completion: ((_ result: Result) -> Void)? = nil - ) -> ImageTask? { - cancelOutstandingTask() - - guard let imageView else { - return nil - } - - self.options = options - - if options.isPrepareForReuseEnabled { // enabled by default -#if os(iOS) || os(tvOS) || os(visionOS) - imageView.layer.removeAllAnimations() -#elseif os(macOS) - let layer = (imageView as? NSView)?.layer ?? imageView.layer - layer?.removeAllAnimations() -#endif - } - - // Handle a scenario where request is `nil` (in the same way as a failure) - guard var request else { - if options.isPrepareForReuseEnabled { - imageView.nuke_display(image: nil, data: nil) - } - let result: Result = .failure(.imageRequestMissing) - handle(result: result, isFromMemory: true) - completion?(result) - return nil - } - - let pipeline = options.pipeline ?? ImagePipeline.shared - if !options.processors.isEmpty && request.processors.isEmpty { - request.processors = options.processors - } - - // Quick synchronous memory cache lookup. - if let image = pipeline.cache[request] { - display(image, true, .success) - if !image.isPreview { // Final image was downloaded - completion?(.success(ImageResponse(container: image, request: request, cacheType: .memory))) - return nil // No task to perform - } - } - - // Display a placeholder. - if let placeholder = options.placeholder { - display(ImageContainer(image: placeholder), true, .placeholder) - } else if options.isPrepareForReuseEnabled { - imageView.nuke_display(image: nil, data: nil) // Remove previously displayed images (if any) - } - - task = pipeline.loadImage(with: request, queue: .main, progress: { [weak self] response, completedCount, totalCount in - if let response, options.isProgressiveRenderingEnabled { - self?.handle(partialImage: response) - } - progress?(response, completedCount, totalCount) - }, completion: { [weak self] result in - self?.handle(result: result, isFromMemory: false) - completion?(result) - }) - return task - } - - func cancelOutstandingTask() { - task?.cancel() // The pipeline guarantees no callbacks to be deliver after cancellation - task = nil - } - - // MARK: - Handling Responses - - private func handle(result: Result, isFromMemory: Bool) { - switch result { - case let .success(response): - display(response.container, isFromMemory, .success) - case .failure: - if let failureImage = options.failureImage { - display(ImageContainer(image: failureImage), isFromMemory, .failure) - } - } - self.task = nil - } - - private func handle(partialImage response: ImageResponse) { - display(response.container, false, .success) - } - -#if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) - - private func display(_ image: ImageContainer, _ isFromMemory: Bool, _ response: ImageLoadingOptions.ResponseType) { - guard let imageView else { - return - } - - var image = image - -#if os(iOS) || os(tvOS) || os(visionOS) - if let tintColor = options.tintColor(for: response) { - image.image = image.image.withRenderingMode(.alwaysTemplate) - imageView.tintColor = tintColor - } -#endif - - if !isFromMemory || options.alwaysTransition, let transition = options.transition(for: response) { - switch transition.style { - case let .fadeIn(params): - runFadeInTransition(image: image, params: params, response: response) - case let .custom(closure): - // The user is responsible for both displaying an image and performing - // animations. - closure(imageView, image.image) - } - } else { - imageView.display(image) - } - -#if os(iOS) || os(tvOS) || os(visionOS) - if let contentMode = options.contentMode(for: response) { - imageView.contentMode = contentMode - } -#endif - } - -#elseif os(watchOS) - - private func display(_ image: ImageContainer, _ isFromMemory: Bool, _ response: ImageLoadingOptions.ResponseType) { - imageView?.display(image) - } - -#endif -} - -// MARK: - ImageViewController (Transitions) - -extension ImageViewController { -#if os(iOS) || os(tvOS) || os(visionOS) - - private func runFadeInTransition(image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters, response: ImageLoadingOptions.ResponseType) { - guard let imageView else { - return - } - - // Special case where it animates between content modes, only works - // on imageView subclasses. - if let contentMode = options.contentMode(for: response), imageView.contentMode != contentMode, let imageView = imageView as? UIImageView, imageView.image != nil { - runCrossDissolveWithContentMode(imageView: imageView, image: image, params: params) - } else { - runSimpleFadeIn(image: image, params: params) - } - } - - private func runSimpleFadeIn(image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters) { - guard let imageView else { - return - } - - UIView.transition( - with: imageView, - duration: params.duration, - options: params.options.union(.transitionCrossDissolve), - animations: { - imageView.nuke_display(image: image.image, data: image.data) - }, - completion: nil - ) - } - - /// Performs cross-dissolve animation alonside transition to a new content - /// mode. This isn't natively supported feature and it requires a second - /// image view. There might be better ways to implement it. - private func runCrossDissolveWithContentMode(imageView: UIImageView, image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters) { - // Lazily create a transition view. - let transitionView = self.transitionImageView - - // Create a transition view which mimics current view's contents. - transitionView.image = imageView.image - transitionView.contentMode = imageView.contentMode - imageView.addSubview(transitionView) - transitionView.frame = imageView.bounds - - // "Manual" cross-fade. - transitionView.alpha = 1 - imageView.alpha = 0 - imageView.display(image) // Display new image in current view - - UIView.animate( - withDuration: params.duration, - delay: 0, - options: params.options, - animations: { - transitionView.alpha = 0 - imageView.alpha = 1 - }, - completion: { isCompleted in - if isCompleted { - transitionView.removeFromSuperview() - } - } - ) - } - -#elseif os(macOS) - - private func runFadeInTransition(image: ImageContainer, params: ImageLoadingOptions.Transition.Parameters, response: ImageLoadingOptions.ResponseType) { - let animation = CABasicAnimation(keyPath: "opacity") - animation.duration = params.duration - animation.fromValue = 0 - animation.toValue = 1 - imageView?.layer?.add(animation, forKey: "imageTransition") - - imageView?.display(image) - } - -#endif -} - -#endif diff --git a/NukeUI/FetchImage.swift b/NukeUI/FetchImage.swift deleted file mode 100644 index 29f309b..0000000 --- a/NukeUI/FetchImage.swift +++ /dev/null @@ -1,248 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import SwiftUI -import Combine - -/// An observable object that simplifies image loading in SwiftUI. -@MainActor -public final class FetchImage: ObservableObject, Identifiable { - /// Returns the current fetch result. - @Published public private(set) var result: Result? - - /// Returns the fetched image. - public var image: Image? { -#if os(macOS) - imageContainer.map { Image(nsImage: $0.image) } -#else - imageContainer.map { Image(uiImage: $0.image) } -#endif - } - - /// Returns the fetched image. - /// - /// - note: In case pipeline has `isProgressiveDecodingEnabled` option enabled - /// and the image being downloaded supports progressive decoding, the `image` - /// might be updated multiple times during the download. - @Published public private(set) var imageContainer: ImageContainer? - - /// Returns `true` if the image is being loaded. - @Published public private(set) var isLoading = false - - /// Animations to be used when displaying the loaded images. By default, `nil`. - /// - /// - note: Animation isn't used when image is available in memory cache. - public var transaction = Transaction(animation: nil) - - /// The progress of the current image download. - public var progress: Progress { - if _progress == nil { - _progress = Progress() - } - return _progress! - } - - private var _progress: Progress? - - /// The download progress. - public final class Progress: ObservableObject { - /// The number of bytes that the task has received. - @Published public internal(set) var completed: Int64 = 0 - - /// A best-guess upper bound on the number of bytes of the resource. - @Published public internal(set) var total: Int64 = 0 - - /// Returns the fraction of the completion. - public var fraction: Float { - guard total > 0 else { return 0 } - return min(1, Float(completed) / Float(total)) - } - } - - /// Updates the priority of the task, even if the task is already running. - /// `nil` by default - public var priority: ImageRequest.Priority? { - didSet { priority.map { imageTask?.priority = $0 } } - } - - /// A pipeline used for performing image requests. - public var pipeline: ImagePipeline = .shared - - /// Image processors to be applied unless the processors are provided in the - /// request. `[]` by default. - public var processors: [any ImageProcessing] = [] - - /// Gets called when the request is started. - public var onStart: ((ImageTask) -> Void)? - - /// Gets called when the current request is completed. - public var onCompletion: ((Result) -> Void)? - - private var imageTask: ImageTask? - private var lastResponse: ImageResponse? - private var cancellable: AnyCancellable? - - deinit { - imageTask?.cancel() - } - - /// Initializes the image. To load an image, use one of the `load()` methods. - public init() {} - - // MARK: Loading Images - - /// Loads an image with the given request. - public func load(_ url: URL?) { - load(url.map { ImageRequest(url: $0) }) - } - - /// Loads an image with the given request. - public func load(_ request: ImageRequest?) { - assert(Thread.isMainThread, "Must be called from the main thread") - - reset() - - guard var request else { - handle(result: .failure(ImagePipeline.Error.imageRequestMissing)) - return - } - - if !processors.isEmpty && request.processors.isEmpty { - request.processors = processors - } - if let priority { - request.priority = priority - } - - // Quick synchronous memory cache lookup - if let image = pipeline.cache[request] { - if image.isPreview { - imageContainer = image // Display progressive image - } else { - let response = ImageResponse(container: image, request: request, cacheType: .memory) - handle(result: .success(response)) - return - } - } - - isLoading = true - - let task = pipeline.loadImage( - with: request, - progress: { [weak self] response, completed, total in - guard let self else { return } - if let response { - withTransaction(self.transaction) { - self.handle(preview: response) - } - } else { - self._progress?.completed = completed - self._progress?.total = total - } - }, - completion: { [weak self] result in - guard let self else { return } - withTransaction(self.transaction) { - self.handle(result: result.mapError { $0 }) - } - } - ) - imageTask = task - onStart?(task) - } - - private func handle(preview: ImageResponse) { - // Display progressively decoded image - self.imageContainer = preview.container - } - - private func handle(result: Result) { - isLoading = false - imageTask = nil - - if case .success(let response) = result { - self.imageContainer = response.container - } - self.result = result - self.onCompletion?(result) - } - - // MARK: Load (Async/Await) - - /// Loads and displays an image using the given async function. - /// - /// - parameter action: Fetched the image. - public func load(_ action: @escaping () async throws -> ImageResponse) { - reset() - isLoading = true - - let task = Task { - do { - let response = try await action() - withTransaction(transaction) { - handle(result: .success(response)) - } - } catch { - handle(result: .failure(error)) - } - } - - cancellable = AnyCancellable { task.cancel() } - } - - // MARK: Load (Combine) - - /// Loads an image with the given publisher. - /// - /// - important: Some `FetchImage` features, such as progress reporting and - /// dynamically changing the request priority, are not available when - /// working with a publisher. - public func load(_ publisher: P) where P.Output == ImageResponse { - reset() - - // Not using `first()` because it should support progressive decoding - isLoading = true - cancellable = publisher.sink(receiveCompletion: { [weak self] completion in - guard let self else { return } - self.isLoading = false - switch completion { - case .finished: - if let response = self.lastResponse { - self.result = .success(response) - } // else was cancelled, do nothing - case .failure(let error): - self.result = .failure(error) - } - }, receiveValue: { [weak self] response in - guard let self else { return } - self.lastResponse = response - self.imageContainer = response.container - }) - } - - // MARK: Cancel - - /// Marks the request as being cancelled. Continues to display a downloaded image. - public func cancel() { - // pipeline-based - imageTask?.cancel() // Guarantees that no more callbacks will be delivered - imageTask = nil - - // publisher-based - cancellable = nil - } - - /// Resets the `FetchImage` instance by cancelling the request and removing - /// all of the state including the loaded image. - public func reset() { - cancel() - - // Avoid publishing unchanged values - if isLoading { isLoading = false } - if imageContainer != nil { imageContainer = nil } - if result != nil { result = nil } - if _progress != nil { _progress = nil } - lastResponse = nil // publisher-only - } -} diff --git a/NukeUI/Internal.swift b/NukeUI/Internal.swift deleted file mode 100644 index cb30e13..0000000 --- a/NukeUI/Internal.swift +++ /dev/null @@ -1,119 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -#if !os(watchOS) - -#if os(macOS) -import AppKit -#else -import UIKit -#endif - -import SwiftUI - -#if os(macOS) -public typealias _PlatformBaseView = NSView -typealias _PlatformImageView = NSImageView -typealias _PlatformColor = NSColor -#else -//public typealias _PlatformBaseView = UIView -typealias _PlatformImageView = UIImageView -typealias _PlatformColor = UIColor -#endif - -extension _PlatformBaseView { - @discardableResult - func pinToSuperview() -> [NSLayoutConstraint] { - translatesAutoresizingMaskIntoConstraints = false - let constraints = [ - topAnchor.constraint(equalTo: superview!.topAnchor), - bottomAnchor.constraint(equalTo: superview!.bottomAnchor), - leftAnchor.constraint(equalTo: superview!.leftAnchor), - rightAnchor.constraint(equalTo: superview!.rightAnchor) - ] - NSLayoutConstraint.activate(constraints) - return constraints - } - - @discardableResult - func centerInSuperview() -> [NSLayoutConstraint] { - translatesAutoresizingMaskIntoConstraints = false - let constraints = [ - centerXAnchor.constraint(equalTo: superview!.centerXAnchor), - centerYAnchor.constraint(equalTo: superview!.centerYAnchor) - ] - NSLayoutConstraint.activate(constraints) - return constraints - } - - @discardableResult - func layout(with position: LazyImageView.SubviewPosition) -> [NSLayoutConstraint] { - switch position { - case .center: return centerInSuperview() - case .fill: return pinToSuperview() - } - } -} - -extension CALayer { - func animateOpacity(duration: CFTimeInterval) { - let animation = CABasicAnimation(keyPath: "opacity") - animation.duration = duration - animation.fromValue = 0 - animation.toValue = 1 - add(animation, forKey: "imageTransition") - } -} - -#if os(macOS) -extension NSView { - func setNeedsUpdateConstraints() { - needsUpdateConstraints = true - } - - func insertSubview(_ subivew: NSView, at index: Int) { - addSubview(subivew, positioned: .below, relativeTo: subviews.first) - } -} - -extension NSColor { - static var secondarySystemBackground: NSColor { - .controlBackgroundColor // Close-enough, but we should define a custom color - } -} -#endif - -#endif - -#if os(tvOS) || os(watchOS) -import UIKit - -extension UIColor { - static var secondarySystemBackground: UIColor { - lightGray.withAlphaComponent(0.5) - } -} -#endif - -//func == (lhs: [any ImageProcessing], rhs: [any ImageProcessing]) -> Bool { -// guard lhs.count == rhs.count else { -// return false -// } -// // Lazily creates `hashableIdentifiers` because for some processors the -// // identifiers might be expensive to compute. -// return zip(lhs, rhs).allSatisfy { -// $0.hashableIdentifier == $1.hashableIdentifier -// } -//} - -//extension ImageRequest { -// var preferredImageId: String { -// if !userInfo.isEmpty, let imageId = userInfo[.imageIdKey] as? String { -// return imageId -// } -// return imageId ?? "" -// } -//} diff --git a/NukeUI/LazyImageState.swift b/NukeUI/LazyImageState.swift deleted file mode 100644 index 6953ed3..0000000 --- a/NukeUI/LazyImageState.swift +++ /dev/null @@ -1,48 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation -import SwiftUI -import Combine - -/// Describes current image state. -@MainActor -public protocol LazyImageState { - /// Returns the current fetch result. - var result: Result? { get } - - /// Returns the fetched image. - /// - /// - note: In case pipeline has `isProgressiveDecodingEnabled` option enabled - /// and the image being downloaded supports progressive decoding, the `image` - /// might be updated multiple times during the download. - var imageContainer: ImageContainer? { get } - - /// Returns `true` if the image is being loaded. - var isLoading: Bool { get } - - /// The progress of the image download. - var progress: FetchImage.Progress { get } -} - -extension LazyImageState { - /// Returns the current error. - public var error: Error? { - if case .failure(let error) = result { - return error - } - return nil - } - - /// Returns an image view. - public var image: Image? { -#if os(macOS) - imageContainer.map { Image(nsImage: $0.image) } -#else - imageContainer.map { Image(uiImage: $0.image) } -#endif - } -} - -extension FetchImage: LazyImageState {} diff --git a/NukeUI/LazyImageView.swift b/NukeUI/LazyImageView.swift deleted file mode 100644 index b02a683..0000000 --- a/NukeUI/LazyImageView.swift +++ /dev/null @@ -1,462 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import Foundation - -#if !os(watchOS) - -#if os(macOS) -import AppKit -#else -import UIKit -#endif - -/// Lazily loads and displays images. -/// -/// ``LazyImageView`` is a ``LazyImage`` counterpart for UIKit and AppKit with the equivalent set of APIs. -/// -/// ```swift -/// let imageView = LazyImageView() -/// imageView.placeholderView = UIActivityIndicatorView() -/// imageView.priority = .high -/// imageView.pipeline = customPipeline -/// imageView.onCompletion = { _ in print("Request completed") } -/// -/// imageView.url = URL(string: "https://example.com/image.jpeg") -/// ```` -@MainActor -public final class LazyImageView: _PlatformBaseView { - - // MARK: Placeholder View - - /// An image to be shown while the request is in progress. - public var placeholderImage: PlatformImage? { - didSet { setPlaceholderImage(placeholderImage) } - } - - /// A view to be shown while the request is in progress. For example, - /// a spinner. - public var placeholderView: _PlatformBaseView? { - didSet { setPlaceholderView(oldValue, placeholderView) } - } - - /// The position of the placeholder. `.fill` by default. - /// - /// It also affects `placeholderImage` because it gets converted to a view. - public var placeholderViewPosition: SubviewPosition = .fill { - didSet { - guard oldValue != placeholderViewPosition, - placeholderView != nil else { return } - setNeedsUpdateConstraints() - } - } - - private var placeholderViewConstraints: [NSLayoutConstraint] = [] - - // MARK: Failure View - - /// An image to be shown if the request fails. - public var failureImage: PlatformImage? { - didSet { setFailureImage(failureImage) } - } - - /// A view to be shown if the request fails. - public var failureView: _PlatformBaseView? { - didSet { setFailureView(oldValue, failureView) } - } - - /// The position of the failure vuew. `.fill` by default. - /// - /// It also affects `failureImage` because it gets converted to a view. - public var failureViewPosition: SubviewPosition = .fill { - didSet { - guard oldValue != failureViewPosition, - failureView != nil else { return } - setNeedsUpdateConstraints() - } - } - - private var failureViewConstraints: [NSLayoutConstraint] = [] - - // MARK: Transition - - /// A animated transition to be performed when displaying a loaded image - /// By default, `.fadeIn(duration: 0.33)`. - public var transition: Transition? - - /// An animated transition. - public enum Transition { - /// Fade-in transition. - case fadeIn(duration: TimeInterval) - /// A custom image view transition. - /// - /// The closure will get called after the image is already displayed but - /// before `imageContainer` value is updated. - case custom(closure: (LazyImageView, ImageContainer) -> Void) - } - - // MARK: Underlying Views - -#if os(macOS) - /// Returns the underlying image view. - public let imageView = NSImageView() -#else - public let imageView = UIImageView() -#endif - - /// Creates a custom view for displaying the given image response. - /// - /// Return `nil` to use the default platform image view. - public var makeImageView: ((ImageContainer) -> _PlatformBaseView?)? - - private var customImageView: _PlatformBaseView? - - // MARK: Managing Image Tasks - - /// Processors to be applied to the image. `nil` by default. - /// - /// If you pass an image requests with a non-empty list of processors as - /// a source, your processors will be applied instead. - public var processors: [any ImageProcessing]? - - /// Sets the priority of the image task. The priorit can be changed - /// dynamically. `nil` by default. - public var priority: ImageRequest.Priority? { - didSet { - if let priority { - imageTask?.priority = priority - } - } - } - - /// Current image task. - public var imageTask: ImageTask? - - /// The pipeline to be used for download. `shared` by default. - public var pipeline: ImagePipeline = .shared - - // MARK: Callbacks - - /// Gets called when the request is started. - public var onStart: ((ImageTask) -> Void)? - - /// Gets called when a progressive image preview is produced. - public var onPreview: ((ImageResponse) -> Void)? - - /// Gets called when the request progress is updated. - public var onProgress: ((ImageTask.Progress) -> Void)? - - /// Gets called when the requests finished successfully. - public var onSuccess: ((ImageResponse) -> Void)? - - /// Gets called when the requests fails. - public var onFailure: ((Error) -> Void)? - - /// Gets called when the request is completed. - public var onCompletion: ((Result) -> Void)? - - // MARK: Other Options - - /// `true` by default. If disabled, progressive image scans will be ignored. - public var isProgressiveImageRenderingEnabled = true - - /// `true` by default. If enabled, the image view will be cleared before the - /// new download is started. You can disable it if you want to keep the - /// previous content while the new download is in progress. - public var isResetEnabled = true - - // MARK: Private - - private var isResetNeeded = false - - // MARK: Initializers - - deinit { - imageTask?.cancel() - } - - override public init(frame: CGRect) { - super.init(frame: frame) - didInit() - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) - didInit() - } - - private func didInit() { - imageView.isHidden = true - addSubview(imageView) - imageView.pinToSuperview() - - placeholderView = { - let view = _PlatformBaseView() - let color = _PlatformColor.secondarySystemBackground -#if os(macOS) - view.wantsLayer = true - view.layer?.backgroundColor = color.cgColor -#else - view.backgroundColor = color -#endif - - return view - }() - - transition = .fadeIn(duration: 0.33) - } - - /// Sets the given URL and immediately starts the download. - public var url: URL? { - get { request?.url } - set { request = newValue.map { ImageRequest(url: $0) } } - } - - /// Sets the given request and immediately starts the download. - public var request: ImageRequest? { - didSet { load(request) } - } - - override public func updateConstraints() { - super.updateConstraints() - - updatePlaceholderViewConstraints() - updateFailureViewConstraints() - } - - /// Cancels current request and prepares the view for reuse. - public func reset() { - cancel() - - imageView.image = nil - imageView.isHidden = true - - customImageView?.removeFromSuperview() - - setPlaceholderViewHidden(true) - setFailureViewHidden(true) - - isResetNeeded = false - } - - /// Cancels current request. - public func cancel() { - imageTask?.cancel() - imageTask = nil - } - - // MARK: Loading Images - - /// Loads an image with the given request. - private func load(_ request: ImageRequest?) { - assert(Thread.isMainThread, "Must be called from the main thread") - - cancel() - - if isResetEnabled { - reset() - } else { - isResetNeeded = true - } - - guard var request else { - handle(result: .failure(ImagePipeline.Error.imageRequestMissing), isSync: true) - return - } - - if let processors, !processors.isEmpty, request.processors.isEmpty { - request.processors = processors - } - if let priority { - request.priority = priority - } - - // Quick synchronous memory cache lookup - if let image = pipeline.cache[request] { - if image.isPreview { - display(image, isFromMemory: true) // Display progressive preview - } else { - let response = ImageResponse(container: image, request: request, cacheType: .memory) - handle(result: .success(response), isSync: true) - return - } - } - - setPlaceholderViewHidden(false) - - let task = pipeline.loadImage( - with: request, - queue: .main, - progress: { [weak self] response, completed, total in - guard let self else { return } - let progress = ImageTask.Progress(completed: completed, total: total) - if let response { - self.handle(preview: response) - self.onPreview?(response) - } else { - self.onProgress?(progress) - } - }, - completion: { [weak self] result in - self?.handle(result: result.mapError { $0 }, isSync: false) - } - ) - imageTask = task - onStart?(task) - } - - private func handle(preview: ImageResponse) { - guard isProgressiveImageRenderingEnabled else { - return - } - setPlaceholderViewHidden(true) - display(preview.container, isFromMemory: false) - } - - private func handle(result: Result, isSync: Bool) { - resetIfNeeded() - setPlaceholderViewHidden(true) - - switch result { - case let .success(response): - display(response.container, isFromMemory: isSync) - case .failure: - setFailureViewHidden(false) - } - - imageTask = nil - switch result { - case .success(let response): onSuccess?(response) - case .failure(let error): onFailure?(error) - } - onCompletion?(result) - } - - private func display(_ container: ImageContainer, isFromMemory: Bool) { - resetIfNeeded() - - if let view = makeImageView?(container) { - addSubview(view) - view.pinToSuperview() - customImageView = view - } else { - imageView.image = container.image - imageView.isHidden = false - } - - if !isFromMemory, let transition = transition { - runTransition(transition, container) - } - } - - // MARK: Private (Placeholder View) - - private func setPlaceholderViewHidden(_ isHidden: Bool) { - placeholderView?.isHidden = isHidden - } - - private func setPlaceholderImage(_ placeholderImage: PlatformImage?) { - guard let placeholderImage else { - placeholderView = nil - return - } - placeholderView = _PlatformImageView(image: placeholderImage) - } - - private func setPlaceholderView(_ oldView: _PlatformBaseView?, _ newView: _PlatformBaseView?) { - if let oldView { - oldView.removeFromSuperview() - } - if let newView { - newView.isHidden = !imageView.isHidden - insertSubview(newView, at: 0) - setNeedsUpdateConstraints() -#if os(iOS) || os(tvOS) || os(visionOS) - if let spinner = newView as? UIActivityIndicatorView { - spinner.startAnimating() - } -#endif - } - } - - private func updatePlaceholderViewConstraints() { - NSLayoutConstraint.deactivate(placeholderViewConstraints) - placeholderViewConstraints = placeholderView?.layout(with: placeholderViewPosition) ?? [] - } - - // MARK: Private (Failure View) - - private func setFailureViewHidden(_ isHidden: Bool) { - failureView?.isHidden = isHidden - } - - private func setFailureImage(_ failureImage: PlatformImage?) { - guard let failureImage else { - failureView = nil - return - } - failureView = _PlatformImageView(image: failureImage) - } - - private func setFailureView(_ oldView: _PlatformBaseView?, _ newView: _PlatformBaseView?) { - if let oldView { - oldView.removeFromSuperview() - } - if let newView { - newView.isHidden = true - insertSubview(newView, at: 0) - setNeedsUpdateConstraints() - } - } - - private func updateFailureViewConstraints() { - NSLayoutConstraint.deactivate(failureViewConstraints) - failureViewConstraints = failureView?.layout(with: failureViewPosition) ?? [] - } - - // MARK: Private (Transitions) - - private func runTransition(_ transition: Transition, _ image: ImageContainer) { - switch transition { - case .fadeIn(let duration): - runFadeInTransition(duration: duration) - case .custom(let closure): - closure(self, image) - } - } - -#if os(macOS) - private func runFadeInTransition(duration: TimeInterval) { - guard !imageView.isHidden else { return } - imageView.layer?.animateOpacity(duration: duration) - } -#else - private func runFadeInTransition(duration: TimeInterval) { - guard !imageView.isHidden else { return } - imageView.alpha = 0 - UIView.animate(withDuration: duration, delay: 0, options: [.allowUserInteraction]) { - self.imageView.alpha = 1 - } - } -#endif - - // MARK: Misc - - public enum SubviewPosition { - /// Center in the superview. - case center - - /// Fill the superview. - case fill - } - - private func resetIfNeeded() { - if isResetNeeded { - reset() - isResetNeeded = false - } - } -} - -#endif diff --git a/NukeVideo/AVDataAsset.swift b/NukeVideo/AVDataAsset.swift deleted file mode 100644 index 42465ae..0000000 --- a/NukeVideo/AVDataAsset.swift +++ /dev/null @@ -1,83 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import AVKit -import Foundation - -extension AssetType { - /// Returns `true` if the asset represents a video file - public var isVideo: Bool { - self == .mp4 || self == .m4v || self == .mov - } -} - -#if !os(watchOS) - -private extension AssetType { - var avFileType: AVFileType? { - switch self { - case .mp4: return .mp4 - case .m4v: return .m4v - case .mov: return .mov - default: return nil - } - } -} - -// This class keeps strong pointer to DataAssetResourceLoader -final class AVDataAsset: AVURLAsset { - private let resourceLoaderDelegate: DataAssetResourceLoader - - init(data: Data, type: AssetType?) { - self.resourceLoaderDelegate = DataAssetResourceLoader( - data: data, - contentType: type?.avFileType?.rawValue ?? AVFileType.mp4.rawValue - ) - - // The URL is irrelevant - let url = URL(string: "in-memory-data://\(UUID().uuidString)") ?? URL(fileURLWithPath: "/dev/null") - super.init(url: url, options: nil) - - resourceLoader.setDelegate(resourceLoaderDelegate, queue: .global()) - } -} - -// This allows LazyImage to play video from memory. -private final class DataAssetResourceLoader: NSObject, AVAssetResourceLoaderDelegate { - private let data: Data - private let contentType: String - - init(data: Data, contentType: String) { - self.data = data - self.contentType = contentType - } - - // MARK: - DataAssetResourceLoader - - func resourceLoader( - _ resourceLoader: AVAssetResourceLoader, - shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest - ) -> Bool { - if let contentRequest = loadingRequest.contentInformationRequest { - contentRequest.contentType = contentType - contentRequest.contentLength = Int64(data.count) - contentRequest.isByteRangeAccessSupported = true - } - - if let dataRequest = loadingRequest.dataRequest { - if dataRequest.requestsAllDataToEndOfResource { - dataRequest.respond(with: data[dataRequest.requestedOffset...]) - } else { - let range = dataRequest.requestedOffset..<(dataRequest.requestedOffset + Int64(dataRequest.requestedLength)) - dataRequest.respond(with: data[range]) - } - } - - loadingRequest.finishLoading() - - return true - } -} - -#endif diff --git a/NukeVideo/ImageDecoders+Video.swift b/NukeVideo/ImageDecoders+Video.swift deleted file mode 100644 index 722a5ed..0000000 --- a/NukeVideo/ImageDecoders+Video.swift +++ /dev/null @@ -1,78 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -#if !os(watchOS) && !os(visionOS) - -import Foundation -import AVKit -import AVFoundation - -extension ImageDecoders { - /// The video decoder. - /// - /// To enable the video decoder, register it with a shared registry: - /// - /// ```swift - /// ImageDecoderRegistry.shared.register(ImageDecoders.Video.init) - /// ``` - public final class Video: ImageDecoding, @unchecked Sendable { - private var didProducePreview = false - private let type: AssetType - public var isAsynchronous: Bool { true } - - private let lock = NSLock() - - public init?(context: ImageDecodingContext) { - guard let type = AssetType(context.data), type.isVideo else { return nil } - self.type = type - } - - public func decode(_ data: Data) throws -> ImageContainer { - ImageContainer(image: PlatformImage(), type: type, data: data, userInfo: [ - .videoAssetKey: AVDataAsset(data: data, type: type) - ]) - } - - public func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? { - lock.lock() - defer { lock.unlock() } - - guard let type = AssetType(data), type.isVideo else { return nil } - guard !didProducePreview else { - return nil // We only need one preview - } - guard let preview = makePreview(for: data, type: type) else { - return nil - } - didProducePreview = true - return ImageContainer(image: preview, type: type, isPreview: true, data: data, userInfo: [ - .videoAssetKey: AVDataAsset(data: data, type: type) - ]) - } - } -} - -extension ImageContainer.UserInfoKey { - /// A key for a video asset (`AVAsset`) - public static let videoAssetKey: ImageContainer.UserInfoKey = "com.github/kean/nuke/video-asset" -} - -private func makePreview(for data: Data, type: AssetType) -> PlatformImage? { - let asset = AVDataAsset(data: data, type: type) - let generator = AVAssetImageGenerator(asset: asset) - guard let cgImage = try? generator.copyCGImage(at: CMTime(value: 0, timescale: 1), actualTime: nil) else { - return nil - } - return PlatformImage(cgImage: cgImage) -} - -#endif - -#if os(macOS) -extension NSImage { - convenience init(cgImage: CGImage) { - self.init(cgImage: cgImage, size: .zero) - } -} -#endif diff --git a/NukeVideo/VideoPlayerView.swift b/NukeVideo/VideoPlayerView.swift deleted file mode 100644 index d1bef4e..0000000 --- a/NukeVideo/VideoPlayerView.swift +++ /dev/null @@ -1,196 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -@preconcurrency import AVKit -import Foundation - -#if os(macOS) -public typealias _PlatformBaseView = NSView -#else -public typealias _PlatformBaseView = UIView -#endif - -@MainActor -public final class VideoPlayerView: _PlatformBaseView { - // MARK: Configuration - - /// `.resizeAspectFill` by default. - public var videoGravity: AVLayerVideoGravity = .resizeAspectFill { - didSet { - _playerLayer?.videoGravity = videoGravity - } - } - - /// `true` by default. If disabled, the video will resize with the frame without animations - public var animatesFrameChanges = true - - /// `true` by default. If disabled, will only play a video once. - public var isLooping = true { - didSet { - guard isLooping != oldValue else { return } - player?.actionAtItemEnd = isLooping ? .none : .pause - if isLooping, !(player?.nowPlaying ?? false) { - restart() - } - } - } - - /// Add if you want to do something at the end of the video - public var onVideoFinished: (() -> Void)? - - // MARK: Initialization - - public var playerLayer: AVPlayerLayer { - if let layer = _playerLayer { - return layer - } - let playerLayer = AVPlayerLayer() -#if os(macOS) - wantsLayer = true - self.layer?.addSublayer(playerLayer) -#else - self.layer.addSublayer(playerLayer) -#endif - playerLayer.frame = bounds - playerLayer.videoGravity = videoGravity - _playerLayer = playerLayer - return playerLayer - } - - private var _playerLayer: AVPlayerLayer? - -#if os(iOS) || os(tvOS) || os(visionOS) - override public func layoutSubviews() { - super.layoutSubviews() - - CATransaction.begin() - CATransaction.setDisableActions(!animatesFrameChanges) - _playerLayer?.frame = bounds - CATransaction.commit() - } -#elseif os(macOS) - override public func layout() { - super.layout() - - CATransaction.begin() - CATransaction.setDisableActions(!animatesFrameChanges) - _playerLayer?.frame = bounds - CATransaction.commit() - } -#endif - - // MARK: Private - - private var player: AVPlayer? { - didSet { - registerNotifications() - } - } - - private var playerObserver: AnyObject? - - public func reset() { - _playerLayer?.player = nil - player = nil - playerObserver = nil - } - - public var asset: AVAsset? { - didSet { assetDidChange() } - } - - private func assetDidChange() { - if asset == nil { - reset() - } - } - - private func registerNotifications() { - NotificationCenter.default.addObserver( - self, - selector: #selector(playerItemDidPlayToEndTimeNotification(_:)), - name: .AVPlayerItemDidPlayToEndTime, - object: player?.currentItem - ) - -#if os(iOS) || os(tvOS) || os(visionOS) - NotificationCenter.default.addObserver( - self, - selector: #selector(applicationWillEnterForeground), - name: UIApplication.willEnterForegroundNotification, - object: nil - ) -#endif - } - - public func restart() { - player?.seek(to: CMTime.zero) - player?.play() - } - - public func play() { - guard let asset else { - return - } - - let playerItem = AVPlayerItem(asset: asset) - let player = AVQueuePlayer(playerItem: playerItem) - player.isMuted = true -#if os(visionOS) - player.preventsAutomaticBackgroundingDuringVideoPlayback = false -#else - player.preventsDisplaySleepDuringVideoPlayback = false -#endif - player.actionAtItemEnd = isLooping ? .none : .pause - self.player = player - - playerLayer.player = player - - playerObserver = player.observe(\.status, options: [.new, .initial]) { player, _ in - Task { @MainActor in - if player.status == .readyToPlay { - player.play() - } - } - } - } - - @objc private func playerItemDidPlayToEndTimeNotification(_ notification: Notification) { - guard let playerItem = notification.object as? AVPlayerItem else { - return - } - if isLooping { - playerItem.seek(to: CMTime.zero, completionHandler: nil) - } else { - onVideoFinished?() - } - } - - @objc private func applicationWillEnterForeground() { - if shouldResumeOnInterruption { - player?.play() - } - } - -#if os(iOS) || os(tvOS) || os(visionOS) - override public func willMove(toWindow newWindow: UIWindow?) { - if newWindow != nil && shouldResumeOnInterruption { - player?.play() - } - } -#endif - - private var shouldResumeOnInterruption: Bool { - return player?.nowPlaying == false && - player?.status == .readyToPlay && - isLooping - } -} - -@MainActor -extension AVPlayer { - var nowPlaying: Bool { - rate != 0 && error == nil - } -} diff --git a/android/src/main/java/com/candlefinance/fasterimage/FasterImageViewManager.kt b/android/src/main/java/com/candlefinance/fasterimage/FasterImageViewManager.kt index 96ee587..695c96a 100644 --- a/android/src/main/java/com/candlefinance/fasterimage/FasterImageViewManager.kt +++ b/android/src/main/java/com/candlefinance/fasterimage/FasterImageViewManager.kt @@ -50,7 +50,7 @@ class FasterImageViewManager : SimpleViewManager() { val base64Placeholder = options.getString("base64Placeholder") val thumbHash = options.getString("thumbhash") val resizeMode = options.getString("resizeMode") - val transitionDuration = options.getDouble("transitionDuration") + val transitionDuration = if (options.hasKey("transitionDuration")) options.getInt("transitionDuration") else 100 val borderRadius = if (options.hasKey("borderRadius")) options.getDouble("borderRadius") else 0.0 val cachePolicy = options.getString("cachePolicy") val failureImage = options.getString("failureImage") diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 1bf752b..8e07958 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,13 +1,13 @@ PODS: - boost (1.76.0) - DoubleConversion (1.1.6) - - FasterImage (1.3.0): - - FasterImage/Nuke (= 1.3.0) - - FasterImage/NukeUI (= 1.3.0) + - FasterImage (1.3.1): + - FasterImage/Nuke (= 1.3.1) + - FasterImage/NukeUI (= 1.3.1) - React-Core - - FasterImage/Nuke (1.3.0): + - FasterImage/Nuke (1.3.1): - React-Core - - FasterImage/NukeUI (1.3.0): + - FasterImage/NukeUI (1.3.1): - React-Core - FBLazyVector (0.71.4) - FBReactNativeSpec (0.71.4): @@ -463,7 +463,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 57d2868c099736d80fcd648bf211b4431e51a558 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 - FasterImage: a89a05162ea08303cfabc5e18c4e34ab1afeadc9 + FasterImage: 5242c751d7d848d0d5410ddcbb71ad11ce310363 FBLazyVector: 446e84642979fff0ba57f3c804c2228a473aeac2 FBReactNativeSpec: 241709e132e3bf1526c1c4f00bc5384dd39dfba9 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9