From cc2789f9f7a8e04f5f659ddb6cc76da74c3a50f0 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 28 Jul 2024 12:32:38 -0400 Subject: [PATCH 01/73] Add ImagePipelineActor --- Nuke.xcodeproj/project.pbxproj | 8 +- Sources/Nuke/ImageTask.swift | 118 ++++++------- Sources/Nuke/Internal/Atomic.swift | 10 ++ Sources/Nuke/Internal/ImagePublisher.swift | 156 +++++++++--------- Sources/Nuke/Internal/RateLimiter.swift | 13 +- Sources/Nuke/Internal/ResumableData.swift | 2 +- .../Pipeline/ImagePipeline+Delegate.swift | 6 - Sources/Nuke/Pipeline/ImagePipeline.swift | 115 ++++++------- .../Nuke/Pipeline/ImagePipelineActor.swift | 13 ++ .../Nuke/Prefetching/ImagePrefetcher.swift | 49 +++--- Sources/Nuke/Tasks/AsyncPipelineTask.swift | 8 +- Sources/Nuke/Tasks/AsyncTask.swift | 15 +- .../Nuke/Tasks/TaskFetchOriginalData.swift | 8 +- .../Nuke/Tasks/TaskFetchOriginalImage.swift | 10 +- .../Nuke/Tasks/TaskFetchWithPublisher.swift | 8 +- Sources/Nuke/Tasks/TaskLoadImage.swift | 12 +- Tests/ImagePipelineObserver.swift | 5 - Tests/NukeExtensions.swift | 2 +- .../ImagePipelineAsyncAwaitTests.swift | 37 +++-- .../ImagePipelineLoadDataTests.swift | 8 +- Tests/NukeTests/ImagePrefetcherTests.swift | 1 + Tests/NukeTests/RateLimiterTests.swift | 2 +- Tests/NukeTests/TaskTests.swift | 2 + 23 files changed, 315 insertions(+), 293 deletions(-) create mode 100644 Sources/Nuke/Pipeline/ImagePipelineActor.swift diff --git a/Nuke.xcodeproj/project.pbxproj b/Nuke.xcodeproj/project.pbxproj index 8d53148ba..4964226d0 100644 --- a/Nuke.xcodeproj/project.pbxproj +++ b/Nuke.xcodeproj/project.pbxproj @@ -12,7 +12,6 @@ 0C09B1661FE9A65700E8FE3B /* fixture.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 0C09B1651FE9A65600E8FE3B /* fixture.jpeg */; }; 0C09B1691FE9A65700E8FE3B /* fixture.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 0C09B1651FE9A65600E8FE3B /* fixture.jpeg */; }; 0C09B16F1FE9A6D800E8FE3B /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C068D1BCA888800089D7F /* Helpers.swift */; }; - 0C0F7BF12287F6EE0034E656 /* TaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0F7BF02287F6EE0034E656 /* TaskTests.swift */; }; 0C0FD5E01CA47FE1002A78FB /* DataLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0FD5D01CA47FE1002A78FB /* DataLoader.swift */; }; 0C0FD5EC1CA47FE1002A78FB /* ImagePipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0FD5D31CA47FE1002A78FB /* ImagePipeline.swift */; }; 0C0FD5FC1CA47FE1002A78FB /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0FD5D71CA47FE1002A78FB /* ImageCache.swift */; }; @@ -108,7 +107,6 @@ 0C64F73D2438371A001983C6 /* img_751.heic in Resources */ = {isa = PBXBuildFile; fileRef = 0C64F73C243836B5001983C6 /* img_751.heic */; }; 0C64F73F243838BF001983C6 /* swift.png in Resources */ = {isa = PBXBuildFile; fileRef = 0C64F73E243838BF001983C6 /* swift.png */; }; 0C68F609208A1F40007DC696 /* ImageDecoderRegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68F608208A1F40007DC696 /* ImageDecoderRegistryTests.swift */; }; - 0C69FA4E1D4E222D00DA9982 /* ImagePrefetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD195291D4348AC00E011BB /* ImagePrefetcherTests.swift */; }; 0C6B5BDB257010B400D763F2 /* image-p3.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 0C6B5BDA257010B400D763F2 /* image-p3.jpg */; }; 0C6B5BE1257010D300D763F2 /* ImagePipelineFormatsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6B5BE0257010D300D763F2 /* ImagePipelineFormatsTests.swift */; }; 0C6CF0CD1DAF789C007B8C0E /* XCTestCaseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C068E1BCA888800089D7F /* XCTestCaseExtensions.swift */; }; @@ -231,6 +229,7 @@ 0CB644C92856807F00916267 /* fixture.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 0C09B1651FE9A65600E8FE3B /* fixture.jpeg */; }; 0CB644CA2856807F00916267 /* swift.png in Resources */ = {isa = PBXBuildFile; fileRef = 0C64F73E243838BF001983C6 /* swift.png */; }; 0CBA07862852DA8B00CE29F4 /* ImagePipeline+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBA07852852DA8B00CE29F4 /* ImagePipeline+Error.swift */; }; + 0CC04B0A2C5698D500F1164D /* ImagePipelineActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC04B092C5698D500F1164D /* ImagePipelineActor.swift */; }; 0CC36A1925B8BC2500811018 /* RateLimiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC36A1825B8BC2500811018 /* RateLimiter.swift */; }; 0CC36A2525B8BC4900811018 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC36A2425B8BC4900811018 /* Operation.swift */; }; 0CC36A2C25B8BC6300811018 /* LinkedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC36A2B25B8BC6300811018 /* LinkedList.swift */; }; @@ -492,6 +491,7 @@ 0CB6449B28567E5400916267 /* ImageViewLoadingOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewLoadingOptionsTests.swift; sourceTree = ""; }; 0CB644AA28567EEA00916267 /* ImageViewExtensionsProgressiveDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewExtensionsProgressiveDecodingTests.swift; sourceTree = ""; }; 0CBA07852852DA8B00CE29F4 /* ImagePipeline+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImagePipeline+Error.swift"; sourceTree = ""; }; + 0CC04B092C5698D500F1164D /* ImagePipelineActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePipelineActor.swift; sourceTree = ""; }; 0CC36A1825B8BC2500811018 /* RateLimiter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimiter.swift; sourceTree = ""; }; 0CC36A2425B8BC4900811018 /* Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = ""; }; 0CC36A2B25B8BC6300811018 /* LinkedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkedList.swift; sourceTree = ""; }; @@ -959,6 +959,7 @@ isa = PBXGroup; children = ( 0C0FD5D31CA47FE1002A78FB /* ImagePipeline.swift */, + 0CC04B092C5698D500F1164D /* ImagePipelineActor.swift */, 0CF1754B22913F9800A8946E /* ImagePipeline+Configuration.swift */, 0C53C8B0263C968200E62D03 /* ImagePipeline+Delegate.swift */, 0C78A2A6263F4E680051E0FF /* ImagePipeline+Cache.swift */, @@ -1619,7 +1620,6 @@ 4480674C2A448C9F00DE7CF8 /* DataPublisherTests.swift in Sources */, 0CD37C9A25BA36D5006C2C36 /* ImagePipelineLoadDataTests.swift in Sources */, 0C75279F1D473AEF00EC6222 /* MockImageProcessor.swift in Sources */, - 0C69FA4E1D4E222D00DA9982 /* ImagePrefetcherTests.swift in Sources */, 0CB26807208F25C2004C83F4 /* DataCacheTests.swift in Sources */, 0C880532242E7B1500F8C5B3 /* ImagePipelineDecodingTests.swift in Sources */, 0C91B0F02438E352007F9100 /* RoundedCornersTests.swift in Sources */, @@ -1668,7 +1668,6 @@ 0C68F609208A1F40007DC696 /* ImageDecoderRegistryTests.swift in Sources */, 0CE6202526543EC700AAB8C3 /* ImagePipelinePublisherTests.swift in Sources */, 0C91B0EE2438E307007F9100 /* CircleTests.swift in Sources */, - 0C0F7BF12287F6EE0034E656 /* TaskTests.swift in Sources */, 0CC6271525BDF7A100466F04 /* ImagePipelineImageCacheTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1725,6 +1724,7 @@ 0C78A2A7263F4E680051E0FF /* ImagePipeline+Cache.swift in Sources */, 0CA4ECD026E68FC000BAC8E5 /* DataCaching.swift in Sources */, 0CA4ECCA26E6868300BAC8E5 /* ImageProcessingOptions.swift in Sources */, + 0CC04B0A2C5698D500F1164D /* ImagePipelineActor.swift in Sources */, 0C53C8B1263C968200E62D03 /* ImagePipeline+Delegate.swift in Sources */, 0CA4ECBC26E6856300BAC8E5 /* ImageDecompression.swift in Sources */, 0CA4ECD326E68FDC00BAC8E5 /* ImageCaching.swift in Sources */, diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index 7f91c077e..cd3e4aaa8 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -18,7 +18,8 @@ import AppKit /// 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 { +@ImagePipelineActor +public final class ImageTask: Hashable { /// An identifier that uniquely identifies the task within a given pipeline. /// Unique only within that pipeline. public let taskId: Int64 @@ -28,15 +29,15 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send /// The priority of the task. The priority can be updated dynamically even /// for a task that is already running. - public var priority: ImageRequest.Priority { - get { withLock { $0.priority } } + public nonisolated var priority: ImageRequest.Priority { + get { nonisolatedState.withLock { $0.priority } } set { setPriority(newValue) } } /// Returns the current download progress. Returns zeros before the download /// is started and the expected size of the resource is known. - public var currentProgress: Progress { - withLock { $0.progress } + public nonisolated var currentProgress: Progress { + nonisolatedState.withLock { $0.progress } } /// The download progress. @@ -59,8 +60,8 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send } /// The current state of the task. - public var state: State { - withLock { $0.state } + public nonisolated var state: State { + nonisolatedState.withLock { $0.state } } /// The state of the image task. @@ -94,7 +95,7 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send } /// The stream of progress updates. - public var progress: AsyncStream { + public nonisolated var progress: AsyncStream { makeStream { if case .progress(let value) = $0 { return value } return nil @@ -105,7 +106,7 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send /// progressive decoding. /// /// - seealso: ``ImagePipeline/Configuration-swift.struct/isProgressiveDecodingEnabled`` - public var previews: AsyncStream { + public nonisolated var previews: AsyncStream { makeStream { if case .preview(let value) = $0 { return value } return nil @@ -115,7 +116,7 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send // MARK: - Events /// The events sent by the pipeline during the task execution. - public var events: AsyncStream { makeStream { $0 } } + public nonisolated var events: AsyncStream { makeStream { $0 } } /// An event produced during the runetime of the task. public enum Event: Sendable { @@ -132,59 +133,57 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send case finished(Result) } - private var publicState: PublicState + private nonisolated let nonisolatedState: ImageTaskNonisolatedState private let isDataTask: Bool private let onEvent: ((Event, ImageTask) -> Void)? - private let lock: os_unfair_lock_t - private let queue: DispatchQueue private weak var pipeline: ImagePipeline? - // State synchronized on `pipeline.queue`. - var _task: Task! + private var _task: Task! var _continuation: UnsafeContinuation? var _state: State = .running private var _events: PassthroughSubject? - deinit { - lock.deinitialize(count: 1) - lock.deallocate() - } - - init(taskId: Int64, request: ImageRequest, isDataTask: Bool, pipeline: ImagePipeline, onEvent: ((Event, ImageTask) -> Void)?) { + nonisolated init(taskId: Int64, request: ImageRequest, isDataTask: Bool, pipeline: ImagePipeline, onEvent: ((Event, ImageTask) -> Void)?) { self.taskId = taskId self.request = request - self.publicState = PublicState(priority: request.priority) + self.nonisolatedState = ImageTaskNonisolatedState(priority: request.priority) self.isDataTask = isDataTask self.pipeline = pipeline - self.queue = pipeline.queue self.onEvent = onEvent - - lock = .allocate(capacity: 1) - lock.initialize(to: os_unfair_lock()) + self._task = Task { @ImagePipelineActor in + try await withUnsafeThrowingContinuation { continuation in + self._continuation = continuation + pipeline.startImageTask(self, isDataTask: isDataTask) + } + } } /// 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() { - let didChange: Bool = withLock { + public nonisolated func cancel() { + let didChange: Bool = nonisolatedState.withLock { guard $0.state == .running else { return false } $0.state = .cancelled return true } guard didChange else { return } // Make sure it gets called once (expensive) - pipeline?.imageTaskCancelCalled(self) + Task { + await pipeline?.imageTaskCancelCalled(self) + } } - private func setPriority(_ newValue: ImageRequest.Priority) { - let didChange: Bool = withLock { + private nonisolated func setPriority(_ newValue: ImageRequest.Priority) { + let didChange: Bool = nonisolatedState.withLock { guard $0.priority != newValue else { return false } $0.priority = newValue return $0.state == .running } guard didChange else { return } - pipeline?.imageTaskUpdatePriorityCalled(self, priority: newValue) + Task { + await pipeline?.imageTaskUpdatePriorityCalled(self, priority: newValue) + } } // MARK: Internals @@ -210,7 +209,7 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send _dispatch(.preview(response)) } case let .progress(value): - withLock { $0.progress = value } + nonisolatedState.withLock { $0.progress = value } _dispatch(.progress(value)) case let .error(error): _finish(.failure(error)) @@ -228,7 +227,7 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send guard _state == .running else { return false } _state = state if onEvent == nil { - withLock { $0.state = state } + nonisolatedState.withLock { $0.state = state } } return true } @@ -262,19 +261,13 @@ public final class ImageTask: Hashable, CustomStringConvertible, @unchecked Send // MARK: Hashable - public func hash(into hasher: inout Hasher) { + public nonisolated func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self).hashValue) } - public static func == (lhs: ImageTask, rhs: ImageTask) -> Bool { + public static nonisolated func == (lhs: ImageTask, rhs: ImageTask) -> Bool { ObjectIdentifier(lhs) == ObjectIdentifier(rhs) } - - // MARK: CustomStringConvertible - - public var description: String { - "ImageTask(id: \(taskId), priority: \(priority), progress: \(currentProgress.completed) / \(currentProgress.total), state: \(state))" - } } @available(*, deprecated, renamed: "ImageTask", message: "Async/Await support was added directly to the existing `ImageTask` type") @@ -283,9 +276,9 @@ public typealias AsyncImageTask = ImageTask // MARK: - ImageTask (Private) extension ImageTask { - private func makeStream(of closure: @Sendable @escaping (Event) -> T?) -> AsyncStream { + private nonisolated func makeStream(of closure: @Sendable @escaping (Event) -> T?) -> AsyncStream { AsyncStream { continuation in - self.queue.async { + Task { @ImagePipelineActor in guard let events = self._makeEventsSubject() else { return continuation.finish() } @@ -309,7 +302,6 @@ extension ImageTask { } } - // Synchronized on `pipeline.queue` private func _makeEventsSubject() -> PassthroughSubject? { guard _state == .running else { return nil @@ -319,19 +311,33 @@ extension ImageTask { } return _events! } +} + +/// Contains the state synchronized using the internal lock. +/// +/// - warning: Must be accessed using `withLock`. +private final class ImageTaskNonisolatedState: @unchecked(Sendable) { + var state: ImageTask.State = .running + var priority: ImageRequest.Priority + var progress = ImageTask.Progress(completed: 0, total: 0) + + private let lock: os_unfair_lock_t + + deinit { + lock.deinitialize(count: 1) + lock.deallocate() + } - private func withLock(_ closure: (inout PublicState) -> T) -> T { - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - return closure(&publicState) + init(priority: ImageRequest.Priority) { + self.priority = priority + + lock = .allocate(capacity: 1) + lock.initialize(to: os_unfair_lock()) } - /// Contains the state synchronized using the internal lock. - /// - /// - warning: Must be accessed using `withLock`. - private struct PublicState { - var state: ImageTask.State = .running - var priority: ImageRequest.Priority - var progress = Progress(completed: 0, total: 0) + func withLock(_ closure: (ImageTaskNonisolatedState) -> T) -> T { + os_unfair_lock_lock(lock) + defer { os_unfair_lock_unlock(lock) } + return closure(self) } } diff --git a/Sources/Nuke/Internal/Atomic.swift b/Sources/Nuke/Internal/Atomic.swift index 43055ce35..7a32f40a4 100644 --- a/Sources/Nuke/Internal/Atomic.swift +++ b/Sources/Nuke/Internal/Atomic.swift @@ -38,3 +38,13 @@ final class Atomic: @unchecked Sendable { return closure(&_value) } } + +extension Atomic where T: BinaryInteger { + func incremented() -> T { + withLock { + let value = $0 + $0 += 1 + return value + } + } +} diff --git a/Sources/Nuke/Internal/ImagePublisher.swift b/Sources/Nuke/Internal/ImagePublisher.swift index 985fbc992..8e83a4feb 100644 --- a/Sources/Nuke/Internal/ImagePublisher.swift +++ b/Sources/Nuke/Internal/ImagePublisher.swift @@ -5,81 +5,83 @@ 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 +#warning("TODO: uncomment/move to NukeLegacy?") - 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, - 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 - } -} +///// 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, +// 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/Sources/Nuke/Internal/RateLimiter.swift b/Sources/Nuke/Internal/RateLimiter.swift index d85c34a41..fa7b6f5f1 100644 --- a/Sources/Nuke/Internal/RateLimiter.swift +++ b/Sources/Nuke/Internal/RateLimiter.swift @@ -13,12 +13,12 @@ import Foundation /// 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 { +@ImagePipelineActor +final class RateLimiter { // 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 @@ -30,8 +30,7 @@ final class RateLimiter: @unchecked Sendable { /// - 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 + nonisolated init(rate: Int = 80, burst: Int = 25) { self.bucket = TokenBucket(rate: Double(rate), burst: Double(burst)) } @@ -56,7 +55,11 @@ final class RateLimiter: @unchecked Sendable { 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() } +#warning("correct?") + Task { + try? await Task.sleep(nanoseconds: UInt64(bounds) * 1_000_000) + self.executePendingTasks() + } } private func executePendingTasks() { diff --git a/Sources/Nuke/Internal/ResumableData.swift b/Sources/Nuke/Internal/ResumableData.swift index 97efc57e2..0a8edb0e7 100644 --- a/Sources/Nuke/Internal/ResumableData.swift +++ b/Sources/Nuke/Internal/ResumableData.swift @@ -6,7 +6,7 @@ import Foundation /// Resumable data support. For more info see: /// - https://developer.apple.com/library/content/qa/qa1761/_index.html -struct ResumableData: @unchecked Sendable { +struct ResumableData: Sendable { let data: Data let validator: String // Either Last-Modified or ETag diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift b/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift index 3e6f4d99d..867737a1b 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift @@ -62,10 +62,6 @@ public protocol ImagePipelineDelegate: AnyObject, Sendable { // MARK: ImageTask - /// 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 receives an event. func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline) @@ -124,8 +120,6 @@ extension ImagePipelineDelegate { return response } - public func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline) {} - public func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline) {} public func imageTaskDidStart(_ task: ImageTask, pipeline: ImagePipeline) {} diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index f25cd07da..d0048e223 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -13,21 +13,22 @@ import UIKit import AppKit #endif -/// The pipeline downloads and caches images, and prepares them for display. -public final class ImagePipeline: @unchecked Sendable { +/// The pipeline downloads and caches images, and prepares them for display. +@ImagePipelineActor +public final class ImagePipeline { /// Returns the shared image pipeline. - public static var shared: ImagePipeline { + public nonisolated static var shared: ImagePipeline { get { _shared.value } set { _shared.value = newValue } } - private static let _shared = Atomic(value: ImagePipeline(configuration: .withURLCache)) + private nonisolated static let _shared = Atomic(value: ImagePipeline(configuration: .withURLCache)) /// The pipeline configuration. - public let configuration: Configuration + public nonisolated let configuration: Configuration /// Provides access to the underlying caching subsystems. - public var cache: ImagePipeline.Cache { .init(pipeline: self) } + public nonisolated var cache: ImagePipeline.Cache { .init(pipeline: self) } let delegate: any ImagePipelineDelegate @@ -38,27 +39,15 @@ public final class ImagePipeline: @unchecked Sendable { private let tasksFetchOriginalImage: TaskPool private let tasksFetchOriginalData: 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 + private nonisolated let nextTaskId = Atomic(value: 0) let rateLimiter: RateLimiter? let id = UUID() var onTaskStarted: ((ImageTask) -> Void)? // Debug purposes deinit { - lock.deinitialize(count: 1) - lock.deallocate() - ResumableDataStorage.shared.unregister(self) } @@ -67,9 +56,9 @@ public final class ImagePipeline: @unchecked Sendable { /// - 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) { + public nonisolated init(configuration: Configuration = Configuration(), delegate: (any ImagePipelineDelegate)? = nil) { self.configuration = configuration - self.rateLimiter = configuration.isRateLimiterEnabled ? RateLimiter(queue: queue) : nil + self.rateLimiter = configuration.isRateLimiterEnabled ? RateLimiter() : nil self.delegate = delegate ?? ImagePipelineDefaultDelegate() (configuration.dataLoader as? DataLoader)?.prefersIncrementalDelivery = configuration.isProgressiveDecodingEnabled @@ -79,9 +68,6 @@ public final class ImagePipeline: @unchecked Sendable { self.tasksFetchOriginalImage = TaskPool(isCoalescingEnabled) self.tasksFetchOriginalData = TaskPool(isCoalescingEnabled) - self.lock = .allocate(capacity: 1) - self.lock.initialize(to: os_unfair_lock()) - ResumableDataStorage.shared.register(self) } @@ -99,7 +85,7 @@ public final class ImagePipeline: @unchecked Sendable { /// - 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) { + public nonisolated convenience init(delegate: (any ImagePipelineDelegate)? = nil, _ configure: (inout ImagePipeline.Configuration) -> Void) { var configuration = ImagePipeline.Configuration() configure(&configuration) self.init(configuration: configuration, delegate: delegate) @@ -107,8 +93,8 @@ public final class ImagePipeline: @unchecked Sendable { /// Invalidates the pipeline and cancels all outstanding tasks. Any new /// requests will immediately fail with ``ImagePipeline/Error/pipelineInvalidated`` error. - public func invalidate() { - queue.async { + public nonisolated func invalidate() { + Task { @ImagePipelineActor in guard !self.isInvalidated else { return } self.isInvalidated = true self.tasks.keys.forEach(self.cancelImageTask) @@ -120,14 +106,14 @@ public final class ImagePipeline: @unchecked Sendable { /// Creates a task with the given URL. /// /// The task starts executing the moment it is created. - public func imageTask(with url: URL) -> ImageTask { + public nonisolated func imageTask(with url: URL) -> ImageTask { imageTask(with: ImageRequest(url: url)) } /// Creates a task with the given request. /// /// The task starts executing the moment it is created. - public func imageTask(with request: ImageRequest) -> ImageTask { + public nonisolated func imageTask(with request: ImageRequest) -> ImageTask { makeStartedImageTask(with: request) } @@ -160,7 +146,7 @@ public final class ImagePipeline: @unchecked Sendable { /// - request: An image request. /// - completion: A closure to be called on the main thread when the request /// is finished. - @discardableResult public func loadImage( + @discardableResult public nonisolated func loadImage( with url: URL, completion: @escaping (_ result: Result) -> Void ) -> ImageTask { @@ -173,7 +159,7 @@ public final class ImagePipeline: @unchecked Sendable { /// - request: An image request. /// - completion: A closure to be called on the main thread when the request /// is finished. - @discardableResult public func loadImage( + @discardableResult public nonisolated func loadImage( with request: ImageRequest, completion: @escaping (_ result: Result) -> Void ) -> ImageTask { @@ -188,7 +174,7 @@ public final class ImagePipeline: @unchecked Sendable { /// the progress is updated. /// - completion: A closure to be called on the main thread when the request /// is finished. - @discardableResult public func loadImage( + @discardableResult public nonisolated func loadImage( with request: ImageRequest, queue: DispatchQueue? = nil, progress: ((_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)?, @@ -199,7 +185,7 @@ public final class ImagePipeline: @unchecked Sendable { }, completion: completion) } - func _loadImage( + nonisolated func _loadImage( with request: ImageRequest, isDataTask: Bool = false, queue callbackQueue: DispatchQueue? = nil, @@ -216,7 +202,8 @@ public final class ImagePipeline: @unchecked Sendable { case .preview(let response): progress?(response, task.currentProgress) case .cancelled: break // The legacy APIs do not send cancellation events case .finished(let result): - _ = task._setState(.completed) // Important to do it on the callback queue + #warning("it should be isolated") + // _ = task._setState(.completed) // Important to do it on the callback queue completion(result) } } @@ -224,26 +211,29 @@ public final class ImagePipeline: @unchecked Sendable { } // nuke-13: requires callbacks to be @MainActor @Sendable or deprecate this entire API - private func dispatchCallback(to callbackQueue: DispatchQueue?, _ closure: @escaping () -> Void) { - let box = UncheckedSendableBox(value: closure) - if callbackQueue === self.queue { - closure() - } else { - (callbackQueue ?? self.configuration._callbackQueue).async { - box.value() - } - } + private nonisolated func dispatchCallback(to callbackQueue: DispatchQueue?, _ closure: @escaping () -> Void) { + #warning("remove this") + closure() + +// let box = UncheckedSendableBox(value: closure) +// if callbackQueue === self.queue { +// closure() +// } else { +// (callbackQueue ?? self.configuration._callbackQueue).async { +// box.value() +// } +// } } // 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 request: ImageRequest, completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void) -> ImageTask { + @discardableResult public nonisolated func loadData(with request: ImageRequest, completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void) -> ImageTask { _loadData(with: request, queue: nil, progress: nil, completion: completion) } - private func _loadData( + private nonisolated func _loadData( with request: ImageRequest, queue: DispatchQueue?, progress progressHandler: ((_ completed: Int64, _ total: Int64) -> Void)?, @@ -273,7 +263,7 @@ public final class ImagePipeline: @unchecked Sendable { /// 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( + @discardableResult public nonisolated func loadData( with request: ImageRequest, queue: DispatchQueue? = nil, progress progressHandler: ((_ completed: Int64, _ total: Int64) -> Void)?, @@ -293,36 +283,25 @@ public final class ImagePipeline: @unchecked Sendable { // MARK: - Loading Images (Combine) /// Returns a publisher which starts a new ``ImageTask`` when a subscriber is added. - public func imagePublisher(with url: URL) -> AnyPublisher { + public nonisolated 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() + public nonisolated func imagePublisher(with request: ImageRequest) -> AnyPublisher { + #warning("TODO: reimplement") + fatalError() +// ImagePublisher(request: request, pipeline: self).eraseToAnyPublisher() } // MARK: - ImageTask (Internal) - private func makeStartedImageTask(with request: ImageRequest, isDataTask: Bool = false, onEvent: ((ImageTask.Event, ImageTask) -> Void)? = nil) -> ImageTask { - let task = ImageTask(taskId: nextTaskId, request: request, isDataTask: isDataTask, pipeline: self, onEvent: onEvent) - // Important to call it before `imageTaskStartCalled` - if !isDataTask { - delegate.imageTaskCreated(task, pipeline: self) - } - task._task = Task { - try await withUnsafeThrowingContinuation { continuation in - self.queue.async { - task._continuation = continuation - self.startImageTask(task, isDataTask: isDataTask) - } - } - } - return task + private nonisolated func makeStartedImageTask(with request: ImageRequest, isDataTask: Bool = false, onEvent: ((ImageTask.Event, ImageTask) -> Void)? = nil) -> ImageTask { + ImageTask(taskId: nextTaskId.incremented(), request: request, isDataTask: isDataTask, pipeline: self, onEvent: onEvent) } // By this time, the task has `continuation` set and is fully wired. - private func startImageTask(_ task: ImageTask, isDataTask: Bool) { + func startImageTask(_ task: ImageTask, isDataTask: Bool) { guard task._state != .cancelled else { // The task gets started asynchronously in a `Task` and cancellation // can happen before the pipeline reached `startImageTask`. In that @@ -348,13 +327,11 @@ public final class ImagePipeline: @unchecked Sendable { // MARK: - Image Task Events func imageTaskCancelCalled(_ task: ImageTask) { - queue.async { self.cancelImageTask(task) } + self.cancelImageTask(task) } func imageTaskUpdatePriorityCalled(_ task: ImageTask, priority: ImageRequest.Priority) { - queue.async { - self.tasks[task]?.setPriority(priority.taskPriority) - } + self.tasks[task]?.setPriority(priority.taskPriority) } func imageTask(_ task: ImageTask, didProcessEvent event: ImageTask.Event, isDataTask: Bool) { diff --git a/Sources/Nuke/Pipeline/ImagePipelineActor.swift b/Sources/Nuke/Pipeline/ImagePipelineActor.swift new file mode 100644 index 000000000..f801cd4dc --- /dev/null +++ b/Sources/Nuke/Pipeline/ImagePipelineActor.swift @@ -0,0 +1,13 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). + +import Foundation + +// swiftlint:disable convenience_type +@globalActor +public struct ImagePipelineActor { + public actor ImagePipelineActor { } + public static let shared = ImagePipelineActor() +} +// swiftlint:enable convenience_type diff --git a/Sources/Nuke/Prefetching/ImagePrefetcher.swift b/Sources/Nuke/Prefetching/ImagePrefetcher.swift index 210ca60fb..ee03f1209 100644 --- a/Sources/Nuke/Prefetching/ImagePrefetcher.swift +++ b/Sources/Nuke/Prefetching/ImagePrefetcher.swift @@ -11,7 +11,8 @@ import Foundation /// /// 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 { +@ImagePipelineActor +public final class ImagePrefetcher { /// Pauses the prefetching. /// /// - note: When you pause, the prefetcher will finish outstanding tasks @@ -27,7 +28,9 @@ public final class ImagePrefetcher: @unchecked Sendable { public var priority: ImageRequest.Priority = .low { didSet { let newValue = priority - pipeline.queue.async { self.didUpdatePriority(to: newValue) } + Task { + self.didUpdatePriority(to: newValue) + } } } @@ -53,7 +56,7 @@ public final class ImagePrefetcher: @unchecked Sendable { public var didComplete: (@MainActor @Sendable () -> Void)? private let pipeline: ImagePipeline - private var tasks = [TaskLoadImageKey: Task]() + private var tasks = [TaskLoadImageKey: PrefetchTask]() private let destination: Destination private var _priority: ImageRequest.Priority = .low let queue = OperationQueue() // internal for testing @@ -64,20 +67,21 @@ public final class ImagePrefetcher: @unchecked Sendable { /// - 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) { + public nonisolated 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 { + Task { @ImagePipelineActor in for task in tasks { task.cancel() } @@ -87,7 +91,7 @@ public final class ImagePrefetcher: @unchecked Sendable { /// Starts prefetching images for the given URL. /// /// See also ``startPrefetching(with:)-718dg`` that works with ``ImageRequest``. - public func startPrefetching(with urls: [URL]) { + public nonisolated func startPrefetching(with urls: [URL]) { startPrefetching(with: urls.map { ImageRequest(url: $0) }) } @@ -101,8 +105,8 @@ public final class ImagePrefetcher: @unchecked Sendable { /// (`.low` by default). /// /// See also ``startPrefetching(with:)-1jef2`` that works with `URL`. - public func startPrefetching(with requests: [ImageRequest]) { - pipeline.queue.async { + public nonisolated func startPrefetching(with requests: [ImageRequest]) { + Task { @ImagePipelineActor in self._startPrefetching(with: requests) } } @@ -126,24 +130,27 @@ public final class ImagePrefetcher: @unchecked Sendable { guard tasks[key] == nil else { return } - let task = Task(request: request, key: key) + let task = PrefetchTask(request: request, key: key) task.operation = queue.add { [weak self] finish in guard let self else { return finish() } - self.loadImage(task: task, finish: finish) + Task { @ImagePipelineActor in + self.loadImage(task: task, finish: finish) + } } tasks[key] = task return } - private func loadImage(task: Task, finish: @escaping () -> Void) { - task.imageTask = pipeline._loadImage(with: task.request, isDataTask: destination == .diskCache, queue: pipeline.queue, progress: nil) { [weak self] _ in + #warning("use async/await") + private func loadImage(task: PrefetchTask, finish: @escaping () -> Void) { + task.imageTask = pipeline._loadImage(with: task.request, isDataTask: destination == .diskCache, progress: nil) { [weak self] _ in self?._remove(task) finish() } task.onCancelled = finish } - private func _remove(_ task: Task) { + private func _remove(_ task: PrefetchTask) { guard tasks[task.key] === task else { return } // Should never happen tasks[task.key] = nil sendCompletionIfNeeded() @@ -172,8 +179,8 @@ public final class ImagePrefetcher: @unchecked Sendable { /// of ``ImagePrefetcher``. /// /// See also ``stopPrefetching(with:)-2tcyq`` that works with `URL`. - public func stopPrefetching(with requests: [ImageRequest]) { - pipeline.queue.async { + public nonisolated func stopPrefetching(with requests: [ImageRequest]) { + Task { @ImagePipelineActor in for request in requests { self._stopPrefetching(with: request) } @@ -187,8 +194,8 @@ public final class ImagePrefetcher: @unchecked Sendable { } /// Stops all prefetching tasks. - public func stopPrefetching() { - pipeline.queue.async { + public nonisolated func stopPrefetching() { + Task { @ImagePipelineActor in self.tasks.values.forEach { $0.cancel() } self.tasks.removeAll() } @@ -202,7 +209,7 @@ public final class ImagePrefetcher: @unchecked Sendable { } } - private final class Task: @unchecked Sendable { + private final class PrefetchTask: @unchecked Sendable { let key: TaskLoadImageKey let request: ImageRequest weak var imageTask: ImageTask? diff --git a/Sources/Nuke/Tasks/AsyncPipelineTask.swift b/Sources/Nuke/Tasks/AsyncPipelineTask.swift index 2865e3bae..2d9cd5a08 100644 --- a/Sources/Nuke/Tasks/AsyncPipelineTask.swift +++ b/Sources/Nuke/Tasks/AsyncPipelineTask.swift @@ -19,6 +19,7 @@ class AsyncPipelineTask: AsyncTask, // Returns all image tasks subscribed to the current pipeline task. // A suboptimal approach just to make the new DiskCachPolicy.automatic work. +@ImagePipelineActor protocol ImageTaskSubscribers { var imageTasks: [ImageTask] { get } } @@ -50,12 +51,9 @@ extension AsyncPipelineTask { guard decoder.isAsynchronous else { return completion(decode()) } - operation = pipeline.configuration.imageDecodingQueue.add { [weak self] in - guard let self else { return } + operation = pipeline.configuration.imageDecodingQueue.add { let response = decode() - self.pipeline.queue.async { - completion(response) - } + completion(response) } } } diff --git a/Sources/Nuke/Tasks/AsyncTask.swift b/Sources/Nuke/Tasks/AsyncTask.swift index e381f51fe..a1c561190 100644 --- a/Sources/Nuke/Tasks/AsyncTask.swift +++ b/Sources/Nuke/Tasks/AsyncTask.swift @@ -14,8 +14,8 @@ import Foundation /// 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 { +@ImagePipelineActor +class AsyncTask: AsyncTaskSubscriptionDelegate { private struct Subscription { let closure: (Event) -> Void @@ -218,6 +218,7 @@ class AsyncTask: AsyncTaskSubscriptionDelegate extension AsyncTask { /// Publishes the results of the task. + @ImagePipelineActor struct Publisher { fileprivate let task: AsyncTask @@ -281,7 +282,8 @@ extension AsyncTask.Event: Equatable where Value: Equatable, Error: Equatable {} /// Represents a subscription to a task. The observer must retain a strong /// reference to a subscription. -struct TaskSubscription: Sendable { +@ImagePipelineActor +struct TaskSubscription { private let task: any AsyncTaskSubscriptionDelegate private let key: TaskSubscriptionKey @@ -311,7 +313,8 @@ struct TaskSubscription: Sendable { } } -private protocol AsyncTaskSubscriptionDelegate: AnyObject, Sendable { +@ImagePipelineActor +private protocol AsyncTaskSubscriptionDelegate: AnyObject { func unsubsribe(key: TaskSubscriptionKey) func setPriority(_ priority: TaskPriority, for observer: TaskSubscriptionKey) } @@ -320,12 +323,12 @@ private typealias TaskSubscriptionKey = Int // MARK: - TaskPool -/// Contains the tasks which haven't completed yet. +@ImagePipelineActor final class TaskPool { private let isCoalescingEnabled: Bool private var map = [Key: AsyncTask]() - init(_ isCoalescingEnabled: Bool) { + nonisolated init(_ isCoalescingEnabled: Bool) { self.isCoalescingEnabled = isCoalescingEnabled } diff --git a/Sources/Nuke/Tasks/TaskFetchOriginalData.swift b/Sources/Nuke/Tasks/TaskFetchOriginalData.swift index 519330f22..317e765a0 100644 --- a/Sources/Nuke/Tasks/TaskFetchOriginalData.swift +++ b/Sources/Nuke/Tasks/TaskFetchOriginalData.swift @@ -6,7 +6,7 @@ import Foundation /// Fetches original image from the data loader (`DataLoading`) and stores it /// in the disk cache (`DataCaching`). -final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)>, @unchecked Sendable { +final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)> { private var urlResponse: URLResponse? private var resumableData: ResumableData? private var resumedDataCount: Int64 = 0 @@ -54,7 +54,7 @@ final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)>, @unc guard let self else { return finish() } - self.pipeline.queue.async { + Task { @ImagePipelineActor in self.loadData(urlRequest: urlRequest, finish: finish) } } @@ -83,14 +83,14 @@ final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)>, @unc 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 { + Task { 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 { + Task { self.dataTaskDidFinish(error: error) } }) diff --git a/Sources/Nuke/Tasks/TaskFetchOriginalImage.swift b/Sources/Nuke/Tasks/TaskFetchOriginalImage.swift index 1f9901de2..c353210bb 100644 --- a/Sources/Nuke/Tasks/TaskFetchOriginalImage.swift +++ b/Sources/Nuke/Tasks/TaskFetchOriginalImage.swift @@ -5,7 +5,7 @@ import Foundation /// Receives data from ``TaskLoadImageData`` and decodes it as it arrives. -final class TaskFetchOriginalImage: AsyncPipelineTask, @unchecked Sendable { +final class TaskFetchOriginalImage: AsyncPipelineTask { private var decoder: (any ImageDecoding)? override func start() { @@ -38,8 +38,12 @@ final class TaskFetchOriginalImage: AsyncPipelineTask, @unchecked return } - decode(context, decoder: decoder) { [weak self] in - self?.didFinishDecoding(context: context, result: $0) + #warning("implement using async/await") + decode(context, decoder: decoder) { [weak self] result in + guard let self else { return } + Task { + await self.didFinishDecoding(context: context, result: result) + } } } diff --git a/Sources/Nuke/Tasks/TaskFetchWithPublisher.swift b/Sources/Nuke/Tasks/TaskFetchWithPublisher.swift index 19faec294..5065604e0 100644 --- a/Sources/Nuke/Tasks/TaskFetchWithPublisher.swift +++ b/Sources/Nuke/Tasks/TaskFetchWithPublisher.swift @@ -6,7 +6,7 @@ import Foundation /// Fetches data using the publisher provided with the request. /// Unlike `TaskFetchOriginalImageData`, there is no resumable data involved. -final class TaskFetchWithPublisher: AsyncPipelineTask<(Data, URLResponse?)>, @unchecked Sendable { +final class TaskFetchWithPublisher: AsyncPipelineTask<(Data, URLResponse?)> { private lazy var data = Data() override func start() { @@ -19,7 +19,7 @@ final class TaskFetchWithPublisher: AsyncPipelineTask<(Data, URLResponse?)>, @un guard let self else { return finish() } - self.pipeline.queue.async { + Task { @ImagePipelineActor in self.loadData { finish() } } } @@ -40,12 +40,12 @@ final class TaskFetchWithPublisher: AsyncPipelineTask<(Data, URLResponse?)>, @un let cancellable = publisher.sink(receiveCompletion: { [weak self] result in finish() // Finish the operation! guard let self else { return } - self.pipeline.queue.async { + Task { @ImagePipelineActor in self.dataTaskDidFinish(result) } }, receiveValue: { [weak self] data in guard let self else { return } - self.pipeline.queue.async { + Task { @ImagePipelineActor in self.data.append(data) } }) diff --git a/Sources/Nuke/Tasks/TaskLoadImage.swift b/Sources/Nuke/Tasks/TaskLoadImage.swift index 2f4b6b7e2..13a2a748a 100644 --- a/Sources/Nuke/Tasks/TaskLoadImage.swift +++ b/Sources/Nuke/Tasks/TaskLoadImage.swift @@ -30,8 +30,12 @@ final class TaskLoadImage: AsyncPipelineTask, @unchecked Sendable guard let decoder = pipeline.delegate.imageDecoder(for: context, pipeline: pipeline) else { return didFinishDecoding(with: nil) } - decode(context, decoder: decoder) { [weak self] in - self?.didFinishDecoding(with: try? $0.get()) + #warning("implement using async/awiat") + decode(context, decoder: decoder) { [weak self] result in + guard let self else { return } + Task { + await self.didFinishDecoding(with: try? result.get()) + } } } @@ -82,7 +86,7 @@ final class TaskLoadImage: AsyncPipelineTask, @unchecked Sendable ImagePipeline.Error.processingFailed(processor: processor, context: context, error: error) } } - self.pipeline.queue.async { + Task { @ImagePipelineActor in self.operation = nil self.didFinishProcessing(result: result, isCompleted: isCompleted) } @@ -117,7 +121,7 @@ final class TaskLoadImage: AsyncPipelineTask, @unchecked Sendable let response = signpost(isCompleted ? "DecompressImage" : "DecompressProgressiveImage") { self.pipeline.delegate.decompress(response: response, request: self.request, pipeline: self.pipeline) } - self.pipeline.queue.async { + Task { @ImagePipelineActor in self.operation = nil self.didReceiveDecompressedImage(response, isCompleted: isCompleted) } diff --git a/Tests/ImagePipelineObserver.swift b/Tests/ImagePipelineObserver.swift index c619c59b8..7fc663818 100644 --- a/Tests/ImagePipelineObserver.swift +++ b/Tests/ImagePipelineObserver.swift @@ -29,11 +29,6 @@ final class ImagePipelineObserver: ImagePipelineDelegate, @unchecked Sendable { lock.unlock() } - func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline) { - onTaskCreated?(task) - append(.created) - } - func imageTaskDidStart(_ task: ImageTask, pipeline: ImagePipeline) { startedTaskCount += 1 NotificationCenter.default.post(name: ImagePipelineObserver.didStartTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task]) diff --git a/Tests/NukeExtensions.swift b/Tests/NukeExtensions.swift index b449836b5..871400dc8 100644 --- a/Tests/NukeExtensions.swift +++ b/Tests/NukeExtensions.swift @@ -29,7 +29,7 @@ extension ImageResponse: Equatable { } extension ImagePipeline { - func reconfigured(_ configure: (inout ImagePipeline.Configuration) -> Void) -> ImagePipeline { + nonisolated func reconfigured(_ configure: (inout ImagePipeline.Configuration) -> Void) -> ImagePipeline { var configuration = self.configuration configure(&configuration) return ImagePipeline(configuration: configuration) diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift index caf15c69b..900c0b2b3 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift @@ -174,24 +174,25 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable { XCTAssertEqual(recordedProgress, []) } - func testCancelAsyncImageTask() async throws { - dataLoader.queue.isSuspended = true - - pipeline.queue.suspend() - let task = pipeline.imageTask(with: Test.url) - observer = NotificationCenter.default.addObserver(forName: MockDataLoader.DidStartTask, object: dataLoader, queue: OperationQueue()) { _ in - task.cancel() - } - pipeline.queue.resume() - - var caughtError: Error? - do { - _ = try await task.image - } catch { - caughtError = error - } - XCTAssertTrue(caughtError is CancellationError) - } + #warning("reimplement") +// func testCancelAsyncImageTask() async throws { +// dataLoader.queue.isSuspended = true +// +// pipeline.queue.suspend() +// let task = pipeline.imageTask(with: Test.url) +// observer = NotificationCenter.default.addObserver(forName: MockDataLoader.DidStartTask, object: dataLoader, queue: OperationQueue()) { _ in +// task.cancel() +// } +// pipeline.queue.resume() +// +// var caughtError: Error? +// do { +// _ = try await task.image +// } catch { +// caughtError = error +// } +// XCTAssertTrue(caughtError is CancellationError) +// } // MARK: - Load Data diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift index 101676764..1daf74b01 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift @@ -425,9 +425,11 @@ extension XCTestCase { dataLoader.isSuspended = true let expectation = self.expectation(description: "registered") expectation.expectedFulfillmentCount = count - pipeline.onTaskStarted = { _ in - expectation.fulfill() - } + +#warning("reimplement") +// pipeline.onTaskStarted = { _ in +// expectation.fulfill() +// } closure() wait(for: [expectation], timeout: 5) dataLoader.isSuspended = false diff --git a/Tests/NukeTests/ImagePrefetcherTests.swift b/Tests/NukeTests/ImagePrefetcherTests.swift index 8856f3111..b0e2c177f 100644 --- a/Tests/NukeTests/ImagePrefetcherTests.swift +++ b/Tests/NukeTests/ImagePrefetcherTests.swift @@ -5,6 +5,7 @@ import XCTest @testable import Nuke +#warning("reimplement (remove from target") final class ImagePrefetcherTests: XCTestCase { private var pipeline: ImagePipeline! private var dataLoader: MockDataLoader! diff --git a/Tests/NukeTests/RateLimiterTests.swift b/Tests/NukeTests/RateLimiterTests.swift index 6a65c8734..a65ae37d1 100644 --- a/Tests/NukeTests/RateLimiterTests.swift +++ b/Tests/NukeTests/RateLimiterTests.swift @@ -19,7 +19,7 @@ class RateLimiterTests: XCTestCase { queue.setSpecific(key: queueKey, value: ()) // Note: we set very short rate to avoid bucket form being refilled too quickly - rateLimiter = RateLimiter(queue: queue, rate: 10, burst: 2) + rateLimiter = RateLimiter(rate: 10, burst: 2) } func testThatBurstIsExecutedimmediately() { diff --git a/Tests/NukeTests/TaskTests.swift b/Tests/NukeTests/TaskTests.swift index 4378ac7bc..20d7f339b 100644 --- a/Tests/NukeTests/TaskTests.swift +++ b/Tests/NukeTests/TaskTests.swift @@ -5,6 +5,8 @@ import XCTest @testable import Nuke +#warning("reimplement") + class TaskTests: XCTestCase { // MARK: - Starter From ed5ae0b6c2ea131ee3b38877b07da93daf900868 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 28 Jul 2024 12:34:54 -0400 Subject: [PATCH 02/73] Remove deprecated APIs from ImagePipeleine --- Sources/Nuke/Pipeline/ImagePipeline.swift | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index d0048e223..205ccf516 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -399,18 +399,4 @@ public final class ImagePipeline { request.publisher == nil ? TaskFetchOriginalData(self, request) : TaskFetchWithPublisher(self, request) } } - - // MARK: - Deprecated - - // Deprecated in Nuke 12.7 - @available(*, deprecated, message: "Please the variant variant that accepts `ImageRequest` as a parameter") - @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) - } - - // Deprecated in Nuke 12.7 - @available(*, deprecated, message: "Please the variant that accepts `ImageRequest` as a parameter") - @discardableResult public func data(for url: URL) async throws -> (Data, URLResponse?) { - try await data(for: ImageRequest(url: url)) - } } From d1226478d90a3f3677b19d5b44b7dc87dc4d5b40 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 28 Jul 2024 12:35:19 -0400 Subject: [PATCH 03/73] Remove AsyncImageTask --- Sources/Nuke/ImageTask.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index cd3e4aaa8..5cf9d74d5 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -270,9 +270,6 @@ public final class ImageTask: Hashable { } } -@available(*, deprecated, renamed: "ImageTask", message: "Async/Await support was added directly to the existing `ImageTask` type") -public typealias AsyncImageTask = ImageTask - // MARK: - ImageTask (Private) extension ImageTask { From 3ce7e92bee093d77aa813d800adb81800432c8d7 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 28 Jul 2024 12:36:40 -0400 Subject: [PATCH 04/73] Remove ImagePipeline.Configuration.callbackQueue parameter --- Nuke.xcodeproj/project.pbxproj | 4 - .../ImagePipeline+Configuration.swift | 10 --- .../ImagePipelineAsyncAwaitTests.swift | 5 -- .../ImagePipelineConfigurationTests.swift | 89 ------------------- 4 files changed, 108 deletions(-) delete mode 100644 Tests/NukeTests/ImagePipelineTests/ImagePipelineConfigurationTests.swift diff --git a/Nuke.xcodeproj/project.pbxproj b/Nuke.xcodeproj/project.pbxproj index 4964226d0..7b3a840de 100644 --- a/Nuke.xcodeproj/project.pbxproj +++ b/Nuke.xcodeproj/project.pbxproj @@ -193,7 +193,6 @@ 0CB0479A2856D9AC00DF9B6D /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB047992856D9AC00DF9B6D /* Cache.swift */; }; 0CB26802208F2565004C83F4 /* DataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB26801208F2565004C83F4 /* DataCache.swift */; }; 0CB26807208F25C2004C83F4 /* DataCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB26806208F25C2004C83F4 /* DataCacheTests.swift */; }; - 0CB2EFD22110F38600F7C63F /* ImagePipelineConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB2EFD12110F38600F7C63F /* ImagePipelineConfigurationTests.swift */; }; 0CB2EFD62110F52C00F7C63F /* RateLimiterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB2EFD52110F52C00F7C63F /* RateLimiterTests.swift */; }; 0CB402D525B6569700F5A241 /* TaskFetchOriginalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB402D425B6569700F5A241 /* TaskFetchOriginalData.swift */; }; 0CB402DB25B656D200F5A241 /* TaskFetchOriginalImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB402DA25B656D200F5A241 /* TaskFetchOriginalImage.swift */; }; @@ -482,7 +481,6 @@ 0CB047992856D9AC00DF9B6D /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; 0CB26801208F2565004C83F4 /* DataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCache.swift; sourceTree = ""; }; 0CB26806208F25C2004C83F4 /* DataCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCacheTests.swift; sourceTree = ""; }; - 0CB2EFD12110F38600F7C63F /* ImagePipelineConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePipelineConfigurationTests.swift; sourceTree = ""; }; 0CB2EFD52110F52C00F7C63F /* RateLimiterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateLimiterTests.swift; sourceTree = ""; }; 0CB402D425B6569700F5A241 /* TaskFetchOriginalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskFetchOriginalData.swift; sourceTree = ""; }; 0CB402DA25B656D200F5A241 /* TaskFetchOriginalImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskFetchOriginalImage.swift; sourceTree = ""; }; @@ -824,7 +822,6 @@ 0CC6271425BDF7A100466F04 /* ImagePipelineImageCacheTests.swift */, 0C6D0A8B20E57C810037B68F /* ImagePipelineDataCacheTests.swift */, 0C9B6E7520B9F3E2001924B8 /* ImagePipelineCoalescingTests.swift */, - 0CB2EFD12110F38600F7C63F /* ImagePipelineConfigurationTests.swift */, 0C2A8CF620970B790013FD65 /* ImagePipelineResumableDataTests.swift */, 0C2A8CFA20970D8D0013FD65 /* ImagePipelineProgressiveDecodingTests.swift */, 2DFD93AF233A6AB300D84DB9 /* ImagePipelineProcessorTests.swift */, @@ -1629,7 +1626,6 @@ 0CE745751D4767B900123F65 /* MockImageDecoder.swift in Sources */, 0C70D9782089017500A49DAC /* ImageDecoderTests.swift in Sources */, 0C88C579263DAF1E0061A008 /* ImagePublisherTests.swift in Sources */, - 0CB2EFD22110F38600F7C63F /* ImagePipelineConfigurationTests.swift in Sources */, 0C7082612640521900C62638 /* MockImageEncoder.swift in Sources */, 0CE6202726546FD100AAB8C3 /* CombineExtensions.swift in Sources */, 0C6B5BE1257010D300D763F2 /* ImagePipelineFormatsTests.swift in Sources */, diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift b/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift index 0ce00f09f..d6e3c430d 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift @@ -118,16 +118,6 @@ extension ImagePipeline { /// `data` schemes) inline without using the data loader. By default, `true`. public var isLocalResourcesSupportEnabled = true - /// A queue on which all callbacks, like `progress` and `completion` - /// callbacks are called. `.main` by default. - @available(*, deprecated, message: "`ImagePipeline` no longer supports changing the callback queue") - public var callbackQueue: DispatchQueue { - get { _callbackQueue } - set { _callbackQueue = newValue } - } - - var _callbackQueue = DispatchQueue.main - // MARK: - Options (Shared) /// `false` by default. If `true`, enables `os_signpost` logging for diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift index 900c0b2b3..cae621203 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift @@ -15,8 +15,6 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable { private var recordedPreviews: [ImageResponse] = [] private var pipelineDelegate = ImagePipelineObserver() private var imageTask: ImageTask? - private let callbackQueue = DispatchQueue(label: "testChangingCallbackQueue") - private let callbackQueueKey = DispatchSpecificKey() override func setUp() { super.setUp() @@ -25,10 +23,7 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable { pipeline = ImagePipeline(delegate: pipelineDelegate) { $0.dataLoader = dataLoader $0.imageCache = nil - $0._callbackQueue = callbackQueue } - - callbackQueue.setSpecific(key: callbackQueueKey, value: ()) } // MARK: - Basics diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineConfigurationTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineConfigurationTests.swift deleted file mode 100644 index 3ef0a1877..000000000 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineConfigurationTests.swift +++ /dev/null @@ -1,89 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import XCTest -@testable import Nuke - -class ImagePipelineConfigurationTests: XCTestCase { - - func testImageIsLoadedWithRateLimiterDisabled() { - // Given - let dataLoader = MockDataLoader() - let pipeline = ImagePipeline { - $0.dataLoader = dataLoader - $0.imageCache = nil - - $0.isRateLimiterEnabled = false - } - - // When/Then - expect(pipeline).toLoadImage(with: Test.request) - wait() - } - - // MARK: DataCache - - func testWithDataCache() { - let pipeline = ImagePipeline(configuration: .withDataCache) - XCTAssertNotNil(pipeline.configuration.dataCache) - } - - // MARK: Changing Callback Queue - - func testChangingCallbackQueueLoadImage() { - // Given - let queue = DispatchQueue(label: "testChangingCallbackQueue") - let queueKey = DispatchSpecificKey() - queue.setSpecific(key: queueKey, value: ()) - - let dataLoader = MockDataLoader() - let pipeline = ImagePipeline { - $0.dataLoader = dataLoader - $0.imageCache = nil - - $0._callbackQueue = queue - } - - // When/Then - let expectation = self.expectation(description: "Image Loaded") - pipeline.loadImage(with: Test.request, progress: { _, _, _ in - XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey)) - }, completion: { _ in - XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey)) - expectation.fulfill() - }) - wait() - } - - func testChangingCallbackQueueLoadData() { - // Given - let queue = DispatchQueue(label: "testChangingCallbackQueue") - let queueKey = DispatchSpecificKey() - queue.setSpecific(key: queueKey, value: ()) - - let dataLoader = MockDataLoader() - let pipeline = ImagePipeline { - $0.dataLoader = dataLoader - $0.imageCache = nil - - $0._callbackQueue = queue - } - - // When/Then - let expectation = self.expectation(description: "Image data Loaded") - pipeline.loadData(with: Test.request, progress: { _, _ in - XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey)) - }, completion: { _ in - XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey)) - expectation.fulfill() - }) - wait() - } - - func testEnablingSignposts() { - ImagePipeline.Configuration.isSignpostLoggingEnabled = false // Just padding - ImagePipeline.Configuration.isSignpostLoggingEnabled = true - ImagePipeline.Configuration.isSignpostLoggingEnabled = false - } -} From 8422630d2f4cf860eb9c58b1590e17a23986e132 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 28 Jul 2024 12:38:28 -0400 Subject: [PATCH 05/73] Add a note about ImagePipeline.Configuration --- Sources/Nuke/Prefetching/ImagePrefetcher.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Nuke/Prefetching/ImagePrefetcher.swift b/Sources/Nuke/Prefetching/ImagePrefetcher.swift index ee03f1209..6f2dec9a3 100644 --- a/Sources/Nuke/Prefetching/ImagePrefetcher.swift +++ b/Sources/Nuke/Prefetching/ImagePrefetcher.swift @@ -13,11 +13,13 @@ import Foundation /// even from the main thread during scrolling. @ImagePipelineActor public final class ImagePrefetcher { + #warning("make these non-isolated") + /// 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 { + public var isPaused: Bool = false { didSet { queue.isSuspended = isPaused } } From 1a469cbea4e887a2ce2e800c918b192c0036577e Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 28 Jul 2024 12:39:58 -0400 Subject: [PATCH 06/73] =?UTF-8?q?Remove=20onTaskCreated=20tests=20?= =?UTF-8?q?=E2=80=93=20this=20situation=20was=20eliminated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/ImagePipelineObserver.swift | 2 -- .../ImagePipelineAsyncAwaitTests.swift | 28 ------------------- 2 files changed, 30 deletions(-) diff --git a/Tests/ImagePipelineObserver.swift b/Tests/ImagePipelineObserver.swift index 7fc663818..3b35ee174 100644 --- a/Tests/ImagePipelineObserver.swift +++ b/Tests/ImagePipelineObserver.swift @@ -19,8 +19,6 @@ final class ImagePipelineObserver: ImagePipelineDelegate, @unchecked Sendable { var events = [ImageTaskEvent]() - var onTaskCreated: ((ImageTask) -> Void)? - private let lock = NSLock() private func append(_ event: ImageTaskEvent) { diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift index cae621203..b4f95eada 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift @@ -84,23 +84,6 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable { XCTAssertTrue(caughtError is CancellationError) } - func testCancelFromTaskCreated() async throws { - dataLoader.queue.isSuspended = true - pipelineDelegate.onTaskCreated = { $0.cancel() } - - let task = Task { - try await pipeline.image(for: Test.url) - } - - var caughtError: Error? - do { - _ = try await task.value - } catch { - caughtError = error - } - XCTAssertTrue(caughtError is CancellationError) - } - func testCancelImmediately() async throws { dataLoader.queue.isSuspended = true @@ -220,17 +203,6 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable { XCTAssertTrue(caughtError is CancellationError) } - func testImageTaskReturnedImmediately() async throws { - // GIVEN - pipelineDelegate.onTaskCreated = { [unowned self] in imageTask = $0 } - - // WHEN - _ = try await pipeline.image(for: Test.request) - - // THEN - XCTAssertNotNil(imageTask) - } - func testProgressUpdated() async throws { // GIVEN dataLoader.results[Test.url] = .success( From e79301248f61155fff94de0b628623483cbcaecb Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 28 Jul 2024 12:41:45 -0400 Subject: [PATCH 07/73] Safer testLoadDataDataLoaded --- .../ImagePipelineTests/ImagePipelineLoadDataTests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift index 1daf74b01..9adba060d 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift @@ -31,7 +31,9 @@ class ImagePipelineLoadDataTests: XCTestCase { func testLoadDataDataLoaded() { let expectation = self.expectation(description: "Image data Loaded") pipeline.loadData(with: Test.request) { result in - let response = try! XCTUnwrap(result.value) + guard let response = result.value else { + return XCTFail() + } XCTAssertEqual(response.data.count, 22789) XCTAssertTrue(Thread.isMainThread) expectation.fulfill() From b72c56b4e6ff710052e26d1a6ec0ac0130def12b Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 28 Jul 2024 12:47:39 -0400 Subject: [PATCH 08/73] Add ImagePublisher back --- Sources/Nuke/ImageTask.swift | 8 -- Sources/Nuke/Internal/ImagePublisher.swift | 156 ++++++++++----------- Sources/Nuke/Pipeline/ImagePipeline.swift | 24 +--- 3 files changed, 83 insertions(+), 105 deletions(-) diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index 5cf9d74d5..f536fb4e7 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -190,16 +190,12 @@ public final class ImageTask: Hashable { /// Gets called when the task is cancelled either by the user or by an /// external event such as session invalidation. - /// - /// synchronized on `pipeline.queue`. func _cancel() { guard _setState(.cancelled) else { return } _dispatch(.cancelled) } /// Gets called when the associated task sends a new event. - /// - /// synchronized on `pipeline.queue`. func _process(_ event: AsyncTask.Event) { switch event { case let .value(response, isCompleted): @@ -216,13 +212,11 @@ public final class ImageTask: Hashable { } } - /// Synchronized on `pipeline.queue`. private func _finish(_ result: Result) { guard _setState(.completed) else { return } _dispatch(.finished(result)) } - /// Synchronized on `pipeline.queue`. func _setState(_ state: State) -> Bool { guard _state == .running else { return false } _state = state @@ -236,8 +230,6 @@ public final class ImageTask: Hashable { /// /// - warning: The task needs to be fully wired (`_continuation` present) /// before it can start sending the events. - /// - /// synchronized on `pipeline.queue`. func _dispatch(_ event: Event) { guard _continuation != nil else { return // Task isn't fully wired yet diff --git a/Sources/Nuke/Internal/ImagePublisher.swift b/Sources/Nuke/Internal/ImagePublisher.swift index 8e83a4feb..985fbc992 100644 --- a/Sources/Nuke/Internal/ImagePublisher.swift +++ b/Sources/Nuke/Internal/ImagePublisher.swift @@ -5,83 +5,81 @@ import Foundation import Combine -#warning("TODO: uncomment/move to NukeLegacy?") +/// 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 -///// 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, -// 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 -// } -//} + 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, + 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/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 205ccf516..eb8999c85 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -201,10 +201,7 @@ public final class ImagePipeline { case .progress(let value): progress?(nil, value) case .preview(let response): progress?(response, task.currentProgress) case .cancelled: break // The legacy APIs do not send cancellation events - case .finished(let result): - #warning("it should be isolated") - // _ = task._setState(.completed) // Important to do it on the callback queue - completion(result) + case .finished(let result): completion(result) } } } @@ -212,17 +209,10 @@ public final class ImagePipeline { // nuke-13: requires callbacks to be @MainActor @Sendable or deprecate this entire API private nonisolated func dispatchCallback(to callbackQueue: DispatchQueue?, _ closure: @escaping () -> Void) { - #warning("remove this") - closure() - -// let box = UncheckedSendableBox(value: closure) -// if callbackQueue === self.queue { -// closure() -// } else { -// (callbackQueue ?? self.configuration._callbackQueue).async { -// box.value() -// } -// } + let box = UncheckedSendableBox(value: closure) + (callbackQueue ?? .main).async { + box.value() + } } // MARK: - Loading Data (Closures) @@ -289,9 +279,7 @@ public final class ImagePipeline { /// Returns a publisher which starts a new ``ImageTask`` when a subscriber is added. public nonisolated func imagePublisher(with request: ImageRequest) -> AnyPublisher { - #warning("TODO: reimplement") - fatalError() -// ImagePublisher(request: request, pipeline: self).eraseToAnyPublisher() + ImagePublisher(request: request, pipeline: self).eraseToAnyPublisher() } // MARK: - ImageTask (Internal) From 8c6f7f239fccec9e9a1644d05b2e84ef06f36c52 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 28 Jul 2024 12:49:50 -0400 Subject: [PATCH 09/73] Use Atomic for ImageTaskNonisolatedState --- Sources/Nuke/ImageTask.swift | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index f536fb4e7..4744fa0f1 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -133,7 +133,7 @@ public final class ImageTask: Hashable { case finished(Result) } - private nonisolated let nonisolatedState: ImageTaskNonisolatedState + private nonisolated let nonisolatedState: Atomic private let isDataTask: Bool private let onEvent: ((Event, ImageTask) -> Void)? private weak var pipeline: ImagePipeline? @@ -146,7 +146,7 @@ public final class ImageTask: Hashable { nonisolated init(taskId: Int64, request: ImageRequest, isDataTask: Bool, pipeline: ImagePipeline, onEvent: ((Event, ImageTask) -> Void)?) { self.taskId = taskId self.request = request - self.nonisolatedState = ImageTaskNonisolatedState(priority: request.priority) + self.nonisolatedState = Atomic(value: ImageTaskNonisolatedState(priority: request.priority)) self.isDataTask = isDataTask self.pipeline = pipeline self.onEvent = onEvent @@ -302,31 +302,8 @@ extension ImageTask { } } -/// Contains the state synchronized using the internal lock. -/// -/// - warning: Must be accessed using `withLock`. -private final class ImageTaskNonisolatedState: @unchecked(Sendable) { +private struct ImageTaskNonisolatedState { var state: ImageTask.State = .running var priority: ImageRequest.Priority var progress = ImageTask.Progress(completed: 0, total: 0) - - private let lock: os_unfair_lock_t - - deinit { - lock.deinitialize(count: 1) - lock.deallocate() - } - - init(priority: ImageRequest.Priority) { - self.priority = priority - - lock = .allocate(capacity: 1) - lock.initialize(to: os_unfair_lock()) - } - - func withLock(_ closure: (ImageTaskNonisolatedState) -> T) -> T { - os_unfair_lock_lock(lock) - defer { os_unfair_lock_unlock(lock) } - return closure(self) - } } From 786bebcf763025edf3fbf67724e971e4e3aa6871 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 28 Jul 2024 12:53:11 -0400 Subject: [PATCH 10/73] Cleanup --- Nuke.xcodeproj/project.pbxproj | 4 ++-- .../ImagePipelineTests/ImagePipelineLoadDataTests.swift | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Nuke.xcodeproj/project.pbxproj b/Nuke.xcodeproj/project.pbxproj index 7b3a840de..868d5f9b2 100644 --- a/Nuke.xcodeproj/project.pbxproj +++ b/Nuke.xcodeproj/project.pbxproj @@ -229,6 +229,7 @@ 0CB644CA2856807F00916267 /* swift.png in Resources */ = {isa = PBXBuildFile; fileRef = 0C64F73E243838BF001983C6 /* swift.png */; }; 0CBA07862852DA8B00CE29F4 /* ImagePipeline+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBA07852852DA8B00CE29F4 /* ImagePipeline+Error.swift */; }; 0CC04B0A2C5698D500F1164D /* ImagePipelineActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC04B092C5698D500F1164D /* ImagePipelineActor.swift */; }; + 0CC04B1C2C56AEF000F1164D /* ImagePipelineLoadDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD37C9925BA36D5006C2C36 /* ImagePipelineLoadDataTests.swift */; }; 0CC36A1925B8BC2500811018 /* RateLimiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC36A1825B8BC2500811018 /* RateLimiter.swift */; }; 0CC36A2525B8BC4900811018 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC36A2425B8BC4900811018 /* Operation.swift */; }; 0CC36A2C25B8BC6300811018 /* LinkedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC36A2B25B8BC6300811018 /* LinkedList.swift */; }; @@ -241,7 +242,6 @@ 0CC6279E25C100E300466F04 /* ImageCachePerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC6279D25C100E300466F04 /* ImageCachePerformanceTests.swift */; }; 0CC627A525C100FA00466F04 /* ImageProcessingPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC627A425C100FA00466F04 /* ImageProcessingPerformanceTests.swift */; }; 0CCBB534217D0B980026F552 /* MockProgressiveDataLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCBB533217D0B980026F552 /* MockProgressiveDataLoader.swift */; }; - 0CD37C9A25BA36D5006C2C36 /* ImagePipelineLoadDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD37C9925BA36D5006C2C36 /* ImagePipelineLoadDataTests.swift */; }; 0CE2D9BA2084FDDD00934B28 /* ImageDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE2D9B92084FDDD00934B28 /* ImageDecoding.swift */; }; 0CE334DB2724563D0017BB8D /* ImageProcessorsProtocolExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE334DA2724563D0017BB8D /* ImageProcessorsProtocolExtensionsTests.swift */; }; 0CE3992D1D4697CE00A87D47 /* ImagePipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE3992C1D4697CE00A87D47 /* ImagePipelineTests.swift */; }; @@ -1615,7 +1615,7 @@ buildActionMask = 2147483647; files = ( 4480674C2A448C9F00DE7CF8 /* DataPublisherTests.swift in Sources */, - 0CD37C9A25BA36D5006C2C36 /* ImagePipelineLoadDataTests.swift in Sources */, + 0CC04B1C2C56AEF000F1164D /* ImagePipelineLoadDataTests.swift in Sources */, 0C75279F1D473AEF00EC6222 /* MockImageProcessor.swift in Sources */, 0CB26807208F25C2004C83F4 /* DataCacheTests.swift in Sources */, 0C880532242E7B1500F8C5B3 /* ImagePipelineDecodingTests.swift in Sources */, diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift index 9adba060d..757983630 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift @@ -427,7 +427,6 @@ extension XCTestCase { dataLoader.isSuspended = true let expectation = self.expectation(description: "registered") expectation.expectedFulfillmentCount = count - #warning("reimplement") // pipeline.onTaskStarted = { _ in // expectation.fulfill() From 6168ab16247c00621e3882a5b9ce5b58df71a1a8 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 28 Jul 2024 17:58:34 -0400 Subject: [PATCH 11/73] Remove deprecated APIs --- Sources/Nuke/Caching/DataCache.swift | 30 ++----------------- .../ImagePipeline+Configuration.swift | 4 --- .../Processing/ImageProcessors+Resize.swift | 4 --- 3 files changed, 3 insertions(+), 35 deletions(-) diff --git a/Sources/Nuke/Caching/DataCache.swift b/Sources/Nuke/Caching/DataCache.swift index 8a63cf1c3..20578f82f 100644 --- a/Sources/Nuke/Caching/DataCache.swift +++ b/Sources/Nuke/Caching/DataCache.swift @@ -48,14 +48,6 @@ public final class DataCache: DataCaching, @unchecked Sendable { /// 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() @@ -143,7 +135,7 @@ public final class DataCache: DataCaching, @unchecked Sendable { guard let url = url(for: key) else { return nil } - return try? decompressed(Data(contentsOf: url)) + return try? Data(contentsOf: url) } /// Returns `true` if the cache contains the data for the given key. @@ -322,33 +314,17 @@ public final class DataCache: DataCaching, @unchecked Sendable { switch change.type { case let .add(data): do { - try compressed(data).write(to: url) + try 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 + try? 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 diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift b/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift index d6e3c430d..7f44379c4 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift @@ -141,10 +141,6 @@ extension ImagePipeline { /// Data loading queue. Default maximum concurrent task count is 6. public var dataLoadingQueue = OperationQueue(maxConcurrentCount: 6) - // Deprecated in Nuke 12.6 - @available(*, deprecated, message: "The pipeline now performs cache lookup on the internal queue, reducing the amount of context switching") - public var dataCachingQueue = OperationQueue(maxConcurrentCount: 2) - /// Image decoding queue. Default maximum concurrent task count is 1. public var imageDecodingQueue = OperationQueue(maxConcurrentCount: 1) diff --git a/Sources/Nuke/Processing/ImageProcessors+Resize.swift b/Sources/Nuke/Processing/ImageProcessors+Resize.swift index 984136c2e..c87a7d935 100644 --- a/Sources/Nuke/Processing/ImageProcessors+Resize.swift +++ b/Sources/Nuke/Processing/ImageProcessors+Resize.swift @@ -19,10 +19,6 @@ extension ImageProcessors { 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: From 4b2f4731f53ba6322b98a310f4c0575a9ec9e97a Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 3 Aug 2024 12:27:28 -0400 Subject: [PATCH 12/73] Update unit tests --- Sources/Nuke/Pipeline/ImagePipeline.swift | 3 ++- .../ImagePipelineTests/ImagePipelineLoadDataTests.swift | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index eb8999c85..a7ec49b18 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -45,7 +45,8 @@ public final class ImagePipeline { let rateLimiter: RateLimiter? let id = UUID() - var onTaskStarted: ((ImageTask) -> Void)? // Debug purposes +#warning("TODO: remove") + nonisolated(unsafe) var onTaskStarted: ((ImageTask) -> Void)? // Debug purposes deinit { ResumableDataStorage.shared.unregister(self) diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift index 757983630..b6cfb27a5 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift @@ -427,10 +427,9 @@ extension XCTestCase { dataLoader.isSuspended = true let expectation = self.expectation(description: "registered") expectation.expectedFulfillmentCount = count -#warning("reimplement") -// pipeline.onTaskStarted = { _ in -// expectation.fulfill() -// } + pipeline.onTaskStarted = { _ in + expectation.fulfill() + } closure() wait(for: [expectation], timeout: 5) dataLoader.isSuspended = false From 287b1dbae04e1a7508dde04cc74ee9e7ab46dbef Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 3 Aug 2024 12:29:38 -0400 Subject: [PATCH 13/73] Rename Atomic to Mutex --- Nuke.xcodeproj/project.pbxproj | 8 ++++---- Sources/Nuke/Encoding/ImageEncoders+ImageIO.swift | 2 +- Sources/Nuke/ImageTask.swift | 4 ++-- Sources/Nuke/Internal/Log.swift | 2 +- Sources/Nuke/Internal/{Atomic.swift => Mutex.swift} | 6 +++--- Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift | 2 +- Sources/Nuke/Pipeline/ImagePipeline.swift | 4 ++-- Sources/Nuke/Processing/ImageProcessors+CoreImage.swift | 2 +- Tests/Helpers.swift | 2 +- Tests/MockDataLoader.swift | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) rename Sources/Nuke/Internal/{Atomic.swift => Mutex.swift} (90%) diff --git a/Nuke.xcodeproj/project.pbxproj b/Nuke.xcodeproj/project.pbxproj index 868d5f9b2..3713754dc 100644 --- a/Nuke.xcodeproj/project.pbxproj +++ b/Nuke.xcodeproj/project.pbxproj @@ -187,7 +187,7 @@ 0CA4ECD026E68FC000BAC8E5 /* DataCaching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA4ECCF26E68FC000BAC8E5 /* DataCaching.swift */; }; 0CA4ECD326E68FDC00BAC8E5 /* ImageCaching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA4ECD226E68FDC00BAC8E5 /* ImageCaching.swift */; }; 0CA5D954263CCEA500E08E17 /* ImagePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA5D953263CCEA500E08E17 /* ImagePublisher.swift */; }; - 0CA8D8ED2958DA3700EDAA2C /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA8D8EC2958DA3700EDAA2C /* Atomic.swift */; }; + 0CA8D8ED2958DA3700EDAA2C /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA8D8EC2958DA3700EDAA2C /* Mutex.swift */; }; 0CAAB0101E45D6DA00924450 /* NukeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAAB00F1E45D6DA00924450 /* NukeExtensions.swift */; }; 0CAAB0131E45D6DA00924450 /* NukeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAAB00F1E45D6DA00924450 /* NukeExtensions.swift */; }; 0CB0479A2856D9AC00DF9B6D /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB047992856D9AC00DF9B6D /* Cache.swift */; }; @@ -476,7 +476,7 @@ 0CA4ECCF26E68FC000BAC8E5 /* DataCaching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCaching.swift; sourceTree = ""; }; 0CA4ECD226E68FDC00BAC8E5 /* ImageCaching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCaching.swift; sourceTree = ""; }; 0CA5D953263CCEA500E08E17 /* ImagePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePublisher.swift; sourceTree = ""; }; - 0CA8D8EC2958DA3700EDAA2C /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; + 0CA8D8EC2958DA3700EDAA2C /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; 0CAAB00F1E45D6DA00924450 /* NukeExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NukeExtensions.swift; sourceTree = ""; }; 0CB047992856D9AC00DF9B6D /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; 0CB26801208F2565004C83F4 /* DataCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCache.swift; sourceTree = ""; }; @@ -995,7 +995,7 @@ 0CC36A3225B8BC7900811018 /* ResumableData.swift */, 0CC36A4025B8BCAC00811018 /* Log.swift */, 0C7150081FC9724C00B880AC /* Extensions.swift */, - 0CA8D8EC2958DA3700EDAA2C /* Atomic.swift */, + 0CA8D8EC2958DA3700EDAA2C /* Mutex.swift */, 0CE6202026542F7200AAB8C3 /* DataPublisher.swift */, 0CA5D953263CCEA500E08E17 /* ImagePublisher.swift */, 0C472F822654AD69007FC0F0 /* ImageRequestKeys.swift */, @@ -1741,7 +1741,7 @@ 0CA4ECBA26E6850B00BAC8E5 /* Graphics.swift in Sources */, 0CA4ECB426E6844B00BAC8E5 /* ImageProcessors.swift in Sources */, 0C2A368B26437BF100F1D000 /* TaskLoadData.swift in Sources */, - 0CA8D8ED2958DA3700EDAA2C /* Atomic.swift in Sources */, + 0CA8D8ED2958DA3700EDAA2C /* Mutex.swift in Sources */, 0C0FD6041CA47FE1002A78FB /* ImageRequest.swift in Sources */, 0CA4EC9926E67CEC00BAC8E5 /* ImageDecoders+Default.swift in Sources */, 0CA4ECC226E685E100BAC8E5 /* ImageProcessors+Composition.swift in Sources */, diff --git a/Sources/Nuke/Encoding/ImageEncoders+ImageIO.swift b/Sources/Nuke/Encoding/ImageEncoders+ImageIO.swift index 415a0f1bb..ba9f6033c 100644 --- a/Sources/Nuke/Encoding/ImageEncoders+ImageIO.swift +++ b/Sources/Nuke/Encoding/ImageEncoders+ImageIO.swift @@ -30,7 +30,7 @@ extension ImageEncoders { self.compressionRatio = compressionRatio } - private static let availability = Atomic<[AssetType: Bool]>(value: [:]) + private static let availability = Mutex<[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 diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index 4744fa0f1..67f580171 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -133,7 +133,7 @@ public final class ImageTask: Hashable { case finished(Result) } - private nonisolated let nonisolatedState: Atomic + private nonisolated let nonisolatedState: Mutex private let isDataTask: Bool private let onEvent: ((Event, ImageTask) -> Void)? private weak var pipeline: ImagePipeline? @@ -146,7 +146,7 @@ public final class ImageTask: Hashable { nonisolated init(taskId: Int64, request: ImageRequest, isDataTask: Bool, pipeline: ImagePipeline, onEvent: ((Event, ImageTask) -> Void)?) { self.taskId = taskId self.request = request - self.nonisolatedState = Atomic(value: ImageTaskNonisolatedState(priority: request.priority)) + self.nonisolatedState = Mutex(ImageTaskNonisolatedState(priority: request.priority)) self.isDataTask = isDataTask self.pipeline = pipeline self.onEvent = onEvent diff --git a/Sources/Nuke/Internal/Log.swift b/Sources/Nuke/Internal/Log.swift index cc725ae4f..a24a17a7d 100644 --- a/Sources/Nuke/Internal/Log.swift +++ b/Sources/Nuke/Internal/Log.swift @@ -24,7 +24,7 @@ func signpost(_ name: StaticString, _ work: () throws -> T) rethrows -> T { return result } -private let log = Atomic(value: OSLog(subsystem: "com.github.kean.Nuke.ImagePipeline", category: "Image Loading")) +private let log = Mutex(OSLog(subsystem: "com.github.kean.Nuke.ImagePipeline", category: "Image Loading")) enum Formatter { static func bytes(_ count: Int) -> String { diff --git a/Sources/Nuke/Internal/Atomic.swift b/Sources/Nuke/Internal/Mutex.swift similarity index 90% rename from Sources/Nuke/Internal/Atomic.swift rename to Sources/Nuke/Internal/Mutex.swift index 7a32f40a4..a01da7c4d 100644 --- a/Sources/Nuke/Internal/Atomic.swift +++ b/Sources/Nuke/Internal/Mutex.swift @@ -4,11 +4,11 @@ import Foundation -final class Atomic: @unchecked Sendable { +final class Mutex: @unchecked Sendable { private var _value: T private let lock: os_unfair_lock_t - init(value: T) { + init(_ value: T) { self._value = value self.lock = .allocate(capacity: 1) self.lock.initialize(to: os_unfair_lock()) @@ -39,7 +39,7 @@ final class Atomic: @unchecked Sendable { } } -extension Atomic where T: BinaryInteger { +extension Mutex where T: BinaryInteger { func incremented() -> T { withLock { let value = $0 diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift b/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift index 7f44379c4..b5fc9b639 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift @@ -130,7 +130,7 @@ extension ImagePipeline { set { _isSignpostLoggingEnabled.value = newValue } } - private static let _isSignpostLoggingEnabled = Atomic(value: false) + private static let _isSignpostLoggingEnabled = Mutex(false) private var isCustomImageCacheProvided = false diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index a7ec49b18..3773af3b6 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -22,7 +22,7 @@ public final class ImagePipeline { set { _shared.value = newValue } } - private nonisolated static let _shared = Atomic(value: ImagePipeline(configuration: .withURLCache)) + private nonisolated static let _shared = Mutex(ImagePipeline(configuration: .withURLCache)) /// The pipeline configuration. public nonisolated let configuration: Configuration @@ -41,7 +41,7 @@ public final class ImagePipeline { private var isInvalidated = false - private nonisolated let nextTaskId = Atomic(value: 0) + private nonisolated let nextTaskId = Mutex(0) let rateLimiter: RateLimiter? let id = UUID() diff --git a/Sources/Nuke/Processing/ImageProcessors+CoreImage.swift b/Sources/Nuke/Processing/ImageProcessors+CoreImage.swift index a199e9120..fb7b2cd25 100644 --- a/Sources/Nuke/Processing/ImageProcessors+CoreImage.swift +++ b/Sources/Nuke/Processing/ImageProcessors+CoreImage.swift @@ -83,7 +83,7 @@ extension ImageProcessors { set { _context.value = newValue } } - private static let _context = Atomic(value: CIContext(options: [.priorityRequestLow: true])) + private static let _context = Mutex(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 { diff --git a/Tests/Helpers.swift b/Tests/Helpers.swift index bc341248e..b6a97d406 100644 --- a/Tests/Helpers.swift +++ b/Tests/Helpers.swift @@ -169,7 +169,7 @@ extension Result { } } -@propertyWrapper final class Atomic { +@propertyWrapper final class Mutex { private var value: T private let lock: os_unfair_lock_t diff --git a/Tests/MockDataLoader.swift b/Tests/MockDataLoader.swift index 272acce19..583898d6e 100644 --- a/Tests/MockDataLoader.swift +++ b/Tests/MockDataLoader.swift @@ -18,7 +18,7 @@ class MockDataLoader: DataLoading, @unchecked Sendable { static let DidStartTask = Notification.Name("com.github.kean.Nuke.Tests.MockDataLoader.DidStartTask") static let DidCancelTask = Notification.Name("com.github.kean.Nuke.Tests.MockDataLoader.DidCancelTask") - @Atomic var createdTaskCount = 0 + @Mutex var createdTaskCount = 0 var results = [URL: Result<(Data, URLResponse), NSError>]() let queue = OperationQueue() var isSuspended: Bool { From f07a2e1f4656b80d69440f095ec65015a70815e1 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 3 Aug 2024 12:33:29 -0400 Subject: [PATCH 14/73] Make some of ImageTask properties nonislated --- Sources/Nuke/ImageTask.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index 67f580171..bf414f6e0 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -22,10 +22,10 @@ import AppKit public final class ImageTask: Hashable { /// An identifier that uniquely identifies the task within a given pipeline. /// Unique only within that pipeline. - public let taskId: Int64 + public nonisolated let taskId: Int64 /// The original request that the task was created with. - public let request: ImageRequest + public nonisolated let request: ImageRequest /// The priority of the task. The priority can be updated dynamically even /// for a task that is already running. From 5a0c7596de0509eb0862cbcd63334c327c188ca7 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 3 Aug 2024 12:45:46 -0400 Subject: [PATCH 15/73] Make ImageTask/continuation private --- Sources/Nuke/ImageTask.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index bf414f6e0..b408168f0 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -139,7 +139,7 @@ public final class ImageTask: Hashable { private weak var pipeline: ImagePipeline? private var _task: Task! - var _continuation: UnsafeContinuation? + private var _continuation: UnsafeContinuation? var _state: State = .running private var _events: PassthroughSubject? From 8983d5dcdff50e659386ea571bbcf44ccb49c573 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 3 Aug 2024 12:51:03 -0400 Subject: [PATCH 16/73] Make ImageTask.State explicitly Sendable --- Sources/Nuke/ImageTask.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index b408168f0..2687cab9e 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -65,7 +65,7 @@ public final class ImageTask: Hashable { } /// The state of the image task. - public enum State { + public enum State: Sendable { /// The task is currently running. case running /// The task has received a cancel message. From 796f5c6e16de2d3114c8e7cb55709f21950af09c Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 15:11:49 -0400 Subject: [PATCH 17/73] Rework isolation in ImageTask --- Sources/Nuke/ImageTask.swift | 127 ++++++++++-------- Sources/Nuke/Pipeline/ImagePipeline.swift | 14 +- Tests/ImagePipelineObserver.swift | 1 + .../ImagePipelineTaskDelegateTests.swift | 1 + 4 files changed, 77 insertions(+), 66 deletions(-) diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index 2687cab9e..e5278533e 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -18,25 +18,24 @@ import AppKit /// 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. -@ImagePipelineActor -public final class ImageTask: Hashable { +public final class ImageTask: Hashable, @unchecked Sendable { /// An identifier that uniquely identifies the task within a given pipeline. /// Unique only within that pipeline. - public nonisolated let taskId: Int64 + public let taskId: Int64 /// The original request that the task was created with. - public nonisolated let request: ImageRequest + public let request: ImageRequest /// The priority of the task. The priority can be updated dynamically even /// for a task that is already running. - public nonisolated var priority: ImageRequest.Priority { + public var priority: ImageRequest.Priority { get { nonisolatedState.withLock { $0.priority } } set { setPriority(newValue) } } /// Returns the current download progress. Returns zeros before the download /// is started and the expected size of the resource is known. - public nonisolated var currentProgress: Progress { + public var currentProgress: Progress { nonisolatedState.withLock { $0.progress } } @@ -60,7 +59,7 @@ public final class ImageTask: Hashable { } /// The current state of the task. - public nonisolated var state: State { + public var state: State { nonisolatedState.withLock { $0.state } } @@ -87,7 +86,7 @@ public final class ImageTask: Hashable { public var response: ImageResponse { get async throws { try await withTaskCancellationHandler { - try await _task.value + try await task.value } onCancel: { cancel() } @@ -95,7 +94,7 @@ public final class ImageTask: Hashable { } /// The stream of progress updates. - public nonisolated var progress: AsyncStream { + public var progress: AsyncStream { makeStream { if case .progress(let value) = $0 { return value } return nil @@ -106,7 +105,7 @@ public final class ImageTask: Hashable { /// progressive decoding. /// /// - seealso: ``ImagePipeline/Configuration-swift.struct/isProgressiveDecodingEnabled`` - public nonisolated var previews: AsyncStream { + public var previews: AsyncStream { makeStream { if case .preview(let value) = $0 { return value } return nil @@ -116,7 +115,7 @@ public final class ImageTask: Hashable { // MARK: - Events /// The events sent by the pipeline during the task execution. - public nonisolated var events: AsyncStream { makeStream { $0 } } + public var events: AsyncStream { makeStream { $0 } } /// An event produced during the runetime of the task. public enum Event: Sendable { @@ -133,28 +132,38 @@ public final class ImageTask: Hashable { case finished(Result) } - private nonisolated let nonisolatedState: Mutex + private let nonisolatedState: Mutex private let isDataTask: Bool private let onEvent: ((Event, ImageTask) -> Void)? + private var task: Task! private weak var pipeline: ImagePipeline? - private var _task: Task! - private var _continuation: UnsafeContinuation? - var _state: State = .running - private var _events: PassthroughSubject? + @ImagePipelineActor + private var context = ImageTaskExecutionContext() - nonisolated init(taskId: Int64, request: ImageRequest, isDataTask: Bool, pipeline: ImagePipeline, onEvent: ((Event, ImageTask) -> Void)?) { + init(taskId: Int64, request: ImageRequest, isDataTask: Bool, pipeline: ImagePipeline, onEvent: ((Event, ImageTask) -> Void)?) { self.taskId = taskId self.request = request - self.nonisolatedState = Mutex(ImageTaskNonisolatedState(priority: request.priority)) + self.nonisolatedState = Mutex(ImageTaskState(priority: request.priority)) self.isDataTask = isDataTask self.pipeline = pipeline self.onEvent = onEvent - self._task = Task { @ImagePipelineActor in - try await withUnsafeThrowingContinuation { continuation in - self._continuation = continuation - pipeline.startImageTask(self, isDataTask: isDataTask) + self.task = Task { + try await perform() + } + } + + @ImagePipelineActor + private func perform() async throws -> ImageResponse { + try await withUnsafeThrowingContinuation { continuation in + context.continuation = continuation + // The task gets started asynchronously in a `Task` and cancellation + // can happen before the pipeline reaches `startImageTask`. In that + // case, the `cancel` method do no send the task event. + guard context.state != .cancelled else { + return _dispatch(.cancelled) // Important to set after continuation } + pipeline?.startImageTask(self, isDataTask: isDataTask) } } @@ -162,27 +171,27 @@ public final class ImageTask: Hashable { /// /// The pipeline will immediately cancel any work associated with a task /// unless there is an equivalent outstanding task running. - public nonisolated func cancel() { + public func cancel() { let didChange: Bool = nonisolatedState.withLock { guard $0.state == .running else { return false } $0.state = .cancelled return true } guard didChange else { return } // Make sure it gets called once (expensive) - Task { - await pipeline?.imageTaskCancelCalled(self) + Task { @ImagePipelineActor in + pipeline?.cancelImageTask(self) } } - private nonisolated func setPriority(_ newValue: ImageRequest.Priority) { + private func setPriority(_ newValue: ImageRequest.Priority) { let didChange: Bool = nonisolatedState.withLock { guard $0.priority != newValue else { return false } $0.priority = newValue return $0.state == .running } guard didChange else { return } - Task { - await pipeline?.imageTaskUpdatePriorityCalled(self, priority: newValue) + Task { @ImagePipelineActor in + pipeline?.imageTaskUpdatePriorityCalled(self, priority: newValue) } } @@ -190,12 +199,14 @@ public final class ImageTask: Hashable { /// Gets called when the task is cancelled either by the user or by an /// external event such as session invalidation. + @ImagePipelineActor func _cancel() { guard _setState(.cancelled) else { return } _dispatch(.cancelled) } /// Gets called when the associated task sends a new event. + @ImagePipelineActor func _process(_ event: AsyncTask.Event) { switch event { case let .value(response, isCompleted): @@ -212,14 +223,16 @@ public final class ImageTask: Hashable { } } + @ImagePipelineActor private func _finish(_ result: Result) { guard _setState(.completed) else { return } _dispatch(.finished(result)) } - func _setState(_ state: State) -> Bool { - guard _state == .running else { return false } - _state = state + @ImagePipelineActor + private func _setState(_ state: State) -> Bool { + guard context.state == .running else { return false } + context.state = state if onEvent == nil { nonisolatedState.withLock { $0.state = state } } @@ -230,19 +243,20 @@ public final class ImageTask: Hashable { /// /// - warning: The task needs to be fully wired (`_continuation` present) /// before it can start sending the events. - func _dispatch(_ event: Event) { - guard _continuation != nil else { + @ImagePipelineActor + private func _dispatch(_ event: Event) { + guard context.continuation != nil else { return // Task isn't fully wired yet } - _events?.send(event) + context.events?.send(event) switch event { case .cancelled: - _events?.send(completion: .finished) - _continuation?.resume(throwing: CancellationError()) + context.events?.send(completion: .finished) + context.continuation?.resume(throwing: CancellationError()) case .finished(let result): let result = result.mapError { $0 as Error } - _events?.send(completion: .finished) - _continuation?.resume(with: result) + context.events?.send(completion: .finished) + context.continuation?.resume(with: result) default: break } @@ -253,11 +267,11 @@ public final class ImageTask: Hashable { // MARK: Hashable - public nonisolated func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self).hashValue) } - public static nonisolated func == (lhs: ImageTask, rhs: ImageTask) -> Bool { + public static func == (lhs: ImageTask, rhs: ImageTask) -> Bool { ObjectIdentifier(lhs) == ObjectIdentifier(rhs) } } @@ -265,10 +279,10 @@ public final class ImageTask: Hashable { // MARK: - ImageTask (Private) extension ImageTask { - private nonisolated func makeStream(of closure: @Sendable @escaping (Event) -> T?) -> AsyncStream { + private func makeStream(of closure: @Sendable @escaping (Event) -> T?) -> AsyncStream { AsyncStream { continuation in Task { @ImagePipelineActor in - guard let events = self._makeEventsSubject() else { + guard let events = self.context.getEvents() else { return continuation.finish() } let cancellable = events.sink { _ in @@ -290,20 +304,27 @@ extension ImageTask { } } } - - private func _makeEventsSubject() -> PassthroughSubject? { - guard _state == .running else { - return nil - } - if _events == nil { - _events = PassthroughSubject() - } - return _events! - } } -private struct ImageTaskNonisolatedState { +private struct ImageTaskState { +#warning("should we just remove it?") var state: ImageTask.State = .running var priority: ImageRequest.Priority var progress = ImageTask.Progress(completed: 0, total: 0) } + +private struct ImageTaskExecutionContext { + var state: ImageTask.State = .running + var continuation: UnsafeContinuation? + var events: PassthroughSubject? + + mutating func getEvents() -> PassthroughSubject? { + guard state == .running else { + return nil + } + if events == nil { + events = PassthroughSubject() + } + return events! + } +} diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 3773af3b6..76d1c5dc9 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -291,12 +291,6 @@ public final class ImagePipeline { // By this time, the task has `continuation` set and is fully wired. func startImageTask(_ task: ImageTask, isDataTask: Bool) { - guard task._state != .cancelled else { - // The task gets started asynchronously in a `Task` and cancellation - // can happen before the pipeline reached `startImageTask`. In that - // case, the `cancel` method do no send the task event. - return task._dispatch(.cancelled) - } guard !isInvalidated else { return task._process(.error(.pipelineInvalidated)) } @@ -308,17 +302,11 @@ public final class ImagePipeline { onTaskStarted?(task) } - private func cancelImageTask(_ task: ImageTask) { + func cancelImageTask(_ task: ImageTask) { tasks.removeValue(forKey: task)?.unsubscribe() task._cancel() } - // MARK: - Image Task Events - - func imageTaskCancelCalled(_ task: ImageTask) { - self.cancelImageTask(task) - } - func imageTaskUpdatePriorityCalled(_ task: ImageTask, priority: ImageRequest.Priority) { self.tasks[task]?.setPriority(priority.taskPriority) } diff --git a/Tests/ImagePipelineObserver.swift b/Tests/ImagePipelineObserver.swift index 3b35ee174..2df19eef8 100644 --- a/Tests/ImagePipelineObserver.swift +++ b/Tests/ImagePipelineObserver.swift @@ -56,6 +56,7 @@ final class ImagePipelineObserver: ImagePipelineDelegate, @unchecked Sendable { } } +#warning("replace with ImageTask.Event") enum ImageTaskEvent: Equatable { case created case started diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineTaskDelegateTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineTaskDelegateTests.swift index 4943d1cfb..c9f5bcbfd 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineTaskDelegateTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineTaskDelegateTests.swift @@ -22,6 +22,7 @@ class ImagePipelineTaskDelegateTests: XCTestCase { } } + #warning("remove ImageTaskEvent.created") func testStartAndCompletedEvents() throws { var result: Result? expect(pipeline).toLoadImage(with: Test.request) { result = $0 } From 039b163924f40a9d6e40e75afddaecb67e404de3 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 17:14:23 -0400 Subject: [PATCH 18/73] Remove nonisolated ImageTask.state --- Sources/Nuke/ImageTask.swift | 45 +++++++++-------------- Sources/Nuke/Pipeline/ImagePipeline.swift | 2 +- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index e5278533e..14fb481fa 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -59,9 +59,8 @@ public final class ImageTask: Hashable, @unchecked Sendable { } /// The current state of the task. - public var state: State { - nonisolatedState.withLock { $0.state } - } + @ImagePipelineActor + public var state: State { context.state } /// The state of the image task. public enum State: Sendable { @@ -73,6 +72,11 @@ public final class ImageTask: Hashable, @unchecked Sendable { case completed } + /// Returns `true` if the task cancellation is initiated. + public var isCancelling: Bool { + nonisolatedState.withLock { $0.isCancelling } + } + // MARK: - Async/Await /// Returns the response image. @@ -173,8 +177,8 @@ public final class ImageTask: Hashable, @unchecked Sendable { /// unless there is an equivalent outstanding task running. public func cancel() { let didChange: Bool = nonisolatedState.withLock { - guard $0.state == .running else { return false } - $0.state = .cancelled + guard !$0.isCancelling else { return false } + $0.isCancelling = true return true } guard didChange else { return } // Make sure it gets called once (expensive) @@ -187,7 +191,7 @@ public final class ImageTask: Hashable, @unchecked Sendable { let didChange: Bool = nonisolatedState.withLock { guard $0.priority != newValue else { return false } $0.priority = newValue - return $0.state == .running + return !$0.isCancelling } guard didChange else { return } Task { @ImagePipelineActor in @@ -201,17 +205,20 @@ public final class ImageTask: Hashable, @unchecked Sendable { /// external event such as session invalidation. @ImagePipelineActor func _cancel() { - guard _setState(.cancelled) else { return } + guard context.state == .running else { return } + context.state = .cancelled _dispatch(.cancelled) } /// Gets called when the associated task sends a new event. @ImagePipelineActor func _process(_ event: AsyncTask.Event) { + guard context.state == .running else { return } switch event { case let .value(response, isCompleted): if isCompleted { - _finish(.success(response)) + context.state = .completed + _dispatch(.finished(.success(response))) } else { _dispatch(.preview(response)) } @@ -219,24 +226,9 @@ public final class ImageTask: Hashable, @unchecked Sendable { nonisolatedState.withLock { $0.progress = value } _dispatch(.progress(value)) case let .error(error): - _finish(.failure(error)) - } - } - - @ImagePipelineActor - private func _finish(_ result: Result) { - guard _setState(.completed) else { return } - _dispatch(.finished(result)) - } - - @ImagePipelineActor - private func _setState(_ state: State) -> Bool { - guard context.state == .running else { return false } - context.state = state - if onEvent == nil { - nonisolatedState.withLock { $0.state = state } + context.state = .completed + _dispatch(.finished(.failure(error))) } - return true } /// Dispatches the given event to the observers. @@ -307,8 +299,7 @@ extension ImageTask { } private struct ImageTaskState { -#warning("should we just remove it?") - var state: ImageTask.State = .running + var isCancelling = false var priority: ImageRequest.Priority var progress = ImageTask.Progress(completed: 0, total: 0) } diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 76d1c5dc9..0cff88326 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -197,7 +197,7 @@ public final class ImagePipeline { self?.dispatchCallback(to: callbackQueue) { // The callback-based API guarantees that after cancellation no // event are called on the callback queue. - guard task.state != .cancelled else { return } + guard !task.isCancelling else { return } switch event { case .progress(let value): progress?(nil, value) case .preview(let response): progress?(response, task.currentProgress) From 109621c40817c9b7895c0f3942414ee5404958e8 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 17:18:41 -0400 Subject: [PATCH 19/73] Remove ImageTaskExecutionContext --- Sources/Nuke/ImageTask.swift | 63 +++++++++++++++++------------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index 14fb481fa..ddded8146 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -60,7 +60,7 @@ public final class ImageTask: Hashable, @unchecked Sendable { /// The current state of the task. @ImagePipelineActor - public var state: State { context.state } + public var state: State = .running /// The state of the image task. public enum State: Sendable { @@ -143,7 +143,10 @@ public final class ImageTask: Hashable, @unchecked Sendable { private weak var pipeline: ImagePipeline? @ImagePipelineActor - private var context = ImageTaskExecutionContext() + var continuation: UnsafeContinuation? + + @ImagePipelineActor + var _events: PassthroughSubject? init(taskId: Int64, request: ImageRequest, isDataTask: Bool, pipeline: ImagePipeline, onEvent: ((Event, ImageTask) -> Void)?) { self.taskId = taskId @@ -159,12 +162,12 @@ public final class ImageTask: Hashable, @unchecked Sendable { @ImagePipelineActor private func perform() async throws -> ImageResponse { - try await withUnsafeThrowingContinuation { continuation in - context.continuation = continuation + try await withUnsafeThrowingContinuation { + continuation = $0 // The task gets started asynchronously in a `Task` and cancellation // can happen before the pipeline reaches `startImageTask`. In that // case, the `cancel` method do no send the task event. - guard context.state != .cancelled else { + guard state != .cancelled else { return _dispatch(.cancelled) // Important to set after continuation } pipeline?.startImageTask(self, isDataTask: isDataTask) @@ -205,19 +208,19 @@ public final class ImageTask: Hashable, @unchecked Sendable { /// external event such as session invalidation. @ImagePipelineActor func _cancel() { - guard context.state == .running else { return } - context.state = .cancelled + guard state == .running else { return } + state = .cancelled _dispatch(.cancelled) } /// Gets called when the associated task sends a new event. @ImagePipelineActor func _process(_ event: AsyncTask.Event) { - guard context.state == .running else { return } + guard state == .running else { return } switch event { case let .value(response, isCompleted): if isCompleted { - context.state = .completed + state = .completed _dispatch(.finished(.success(response))) } else { _dispatch(.preview(response)) @@ -226,7 +229,7 @@ public final class ImageTask: Hashable, @unchecked Sendable { nonisolatedState.withLock { $0.progress = value } _dispatch(.progress(value)) case let .error(error): - context.state = .completed + state = .completed _dispatch(.finished(.failure(error))) } } @@ -237,18 +240,18 @@ public final class ImageTask: Hashable, @unchecked Sendable { /// before it can start sending the events. @ImagePipelineActor private func _dispatch(_ event: Event) { - guard context.continuation != nil else { + guard continuation != nil else { return // Task isn't fully wired yet } - context.events?.send(event) + _events?.send(event) switch event { case .cancelled: - context.events?.send(completion: .finished) - context.continuation?.resume(throwing: CancellationError()) + _events?.send(completion: .finished) + continuation?.resume(throwing: CancellationError()) case .finished(let result): let result = result.mapError { $0 as Error } - context.events?.send(completion: .finished) - context.continuation?.resume(with: result) + _events?.send(completion: .finished) + continuation?.resume(with: result) default: break } @@ -274,10 +277,10 @@ extension ImageTask { private func makeStream(of closure: @Sendable @escaping (Event) -> T?) -> AsyncStream { AsyncStream { continuation in Task { @ImagePipelineActor in - guard let events = self.context.getEvents() else { + guard state == .running else { return continuation.finish() } - let cancellable = events.sink { _ in + let cancellable = makeEvents().sink { _ in continuation.finish() } receiveValue: { event in if let value = closure(event) { @@ -296,6 +299,14 @@ extension ImageTask { } } } + + @ImagePipelineActor + private func makeEvents() -> PassthroughSubject { + if _events == nil { + _events = PassthroughSubject() + } + return _events! + } } private struct ImageTaskState { @@ -303,19 +314,3 @@ private struct ImageTaskState { var priority: ImageRequest.Priority var progress = ImageTask.Progress(completed: 0, total: 0) } - -private struct ImageTaskExecutionContext { - var state: ImageTask.State = .running - var continuation: UnsafeContinuation? - var events: PassthroughSubject? - - mutating func getEvents() -> PassthroughSubject? { - guard state == .running else { - return nil - } - if events == nil { - events = PassthroughSubject() - } - return events! - } -} From 00d6f18203807f17690bb2712dd671a0e3020720 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 17:20:52 -0400 Subject: [PATCH 20/73] Cleanup ImageTask --- Sources/Nuke/ImageTask.swift | 26 +++++++++++------------ Sources/Nuke/Pipeline/ImagePipeline.swift | 4 ++-- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index ddded8146..4e7b8485d 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -168,7 +168,7 @@ public final class ImageTask: Hashable, @unchecked Sendable { // can happen before the pipeline reaches `startImageTask`. In that // case, the `cancel` method do no send the task event. guard state != .cancelled else { - return _dispatch(.cancelled) // Important to set after continuation + return dispatch(.cancelled) // Important to set after continuation } pipeline?.startImageTask(self, isDataTask: isDataTask) } @@ -179,24 +179,22 @@ public final class ImageTask: Hashable, @unchecked Sendable { /// The pipeline will immediately cancel any work associated with a task /// unless there is an equivalent outstanding task running. public func cancel() { - let didChange: Bool = nonisolatedState.withLock { + guard nonisolatedState.withLock({ guard !$0.isCancelling else { return false } $0.isCancelling = true return true - } - guard didChange else { return } // Make sure it gets called once (expensive) + }) else { return } Task { @ImagePipelineActor in pipeline?.cancelImageTask(self) } } private func setPriority(_ newValue: ImageRequest.Priority) { - let didChange: Bool = nonisolatedState.withLock { + guard nonisolatedState.withLock({ guard $0.priority != newValue else { return false } $0.priority = newValue return !$0.isCancelling - } - guard didChange else { return } + }) else { return } Task { @ImagePipelineActor in pipeline?.imageTaskUpdatePriorityCalled(self, priority: newValue) } @@ -210,27 +208,27 @@ public final class ImageTask: Hashable, @unchecked Sendable { func _cancel() { guard state == .running else { return } state = .cancelled - _dispatch(.cancelled) + dispatch(.cancelled) } /// Gets called when the associated task sends a new event. @ImagePipelineActor - func _process(_ event: AsyncTask.Event) { + func process(_ event: AsyncTask.Event) { guard state == .running else { return } switch event { case let .value(response, isCompleted): if isCompleted { state = .completed - _dispatch(.finished(.success(response))) + dispatch(.finished(.success(response))) } else { - _dispatch(.preview(response)) + dispatch(.preview(response)) } case let .progress(value): nonisolatedState.withLock { $0.progress = value } - _dispatch(.progress(value)) + dispatch(.progress(value)) case let .error(error): state = .completed - _dispatch(.finished(.failure(error))) + dispatch(.finished(.failure(error))) } } @@ -239,7 +237,7 @@ public final class ImageTask: Hashable, @unchecked Sendable { /// - warning: The task needs to be fully wired (`_continuation` present) /// before it can start sending the events. @ImagePipelineActor - private func _dispatch(_ event: Event) { + private func dispatch(_ event: Event) { guard continuation != nil else { return // Task isn't fully wired yet } diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 0cff88326..6b9577b4c 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -292,11 +292,11 @@ public final class ImagePipeline { // By this time, the task has `continuation` set and is fully wired. func startImageTask(_ task: ImageTask, isDataTask: Bool) { guard !isInvalidated else { - return task._process(.error(.pipelineInvalidated)) + return task.process(.error(.pipelineInvalidated)) } let worker = isDataTask ? makeTaskLoadData(for: task.request) : makeTaskLoadImage(for: task.request) tasks[task] = worker.subscribe(priority: task.priority.taskPriority, subscriber: task) { [weak task] in - task?._process($0) + task?.process($0) } delegate.imageTaskDidStart(task, pipeline: self) onTaskStarted?(task) From a774d5136fa2ccc404dcbe947e113a3810d98412 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 17:25:38 -0400 Subject: [PATCH 21/73] Revert imageTaskCreated removal --- Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift | 8 +++++++- Sources/Nuke/Pipeline/ImagePipeline.swift | 4 +++- Tests/ImagePipelineObserver.swift | 4 ++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift b/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift index 867737a1b..a3c04fdba 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift @@ -62,6 +62,10 @@ public protocol ImagePipelineDelegate: AnyObject, Sendable { // MARK: ImageTask + /// 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 receives an event. func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline) @@ -86,7 +90,7 @@ extension ImagePipelineDelegate { pipeline.configuration.imageCache } - public func dataLoader(for request: ImageRequest, pipeline: ImagePipeline) -> any DataLoading { + public func dataLoader(for request: ImageRequest,it st pipeline: ImagePipeline) -> any DataLoading { pipeline.configuration.dataLoader } @@ -120,6 +124,8 @@ extension ImagePipelineDelegate { return response } + public func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline) {} + public func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline) {} public func imageTaskDidStart(_ task: ImageTask, pipeline: ImagePipeline) {} diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 6b9577b4c..86e1bb39e 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -286,7 +286,9 @@ public final class ImagePipeline { // MARK: - ImageTask (Internal) private nonisolated func makeStartedImageTask(with request: ImageRequest, isDataTask: Bool = false, onEvent: ((ImageTask.Event, ImageTask) -> Void)? = nil) -> ImageTask { - ImageTask(taskId: nextTaskId.incremented(), request: request, isDataTask: isDataTask, pipeline: self, onEvent: onEvent) + let task = ImageTask(taskId: nextTaskId.incremented(), request: request, isDataTask: isDataTask, pipeline: self, onEvent: onEvent) + delegate.imageTaskCreated(task, pipeline: self) + return task } // By this time, the task has `continuation` set and is fully wired. diff --git a/Tests/ImagePipelineObserver.swift b/Tests/ImagePipelineObserver.swift index 2df19eef8..09a626fe4 100644 --- a/Tests/ImagePipelineObserver.swift +++ b/Tests/ImagePipelineObserver.swift @@ -27,6 +27,10 @@ final class ImagePipelineObserver: ImagePipelineDelegate, @unchecked Sendable { lock.unlock() } + func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline) { + append(.created) + } + func imageTaskDidStart(_ task: ImageTask, pipeline: ImagePipeline) { startedTaskCount += 1 NotificationCenter.default.post(name: ImagePipelineObserver.didStartTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task]) From 4b21e2eabd5877641ac479fbf4d82add0cc97499 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 17:25:45 -0400 Subject: [PATCH 22/73] Revert imageTaskCreated removal --- Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift b/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift index a3c04fdba..3e6f4d99d 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift @@ -90,7 +90,7 @@ extension ImagePipelineDelegate { pipeline.configuration.imageCache } - public func dataLoader(for request: ImageRequest,it st pipeline: ImagePipeline) -> any DataLoading { + public func dataLoader(for request: ImageRequest, pipeline: ImagePipeline) -> any DataLoading { pipeline.configuration.dataLoader } From c56dbae0b6f17a6f2d232205b43848634b17905d Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 17:35:10 -0400 Subject: [PATCH 23/73] Remove soft-deprecated APIs from ImagePipeline.Delegate --- .../Pipeline/ImagePipeline+Delegate.swift | 25 ------ Sources/Nuke/Pipeline/ImagePipeline.swift | 12 --- Sources/NukeVideo/VideoPlayerView.swift | 5 -- Tests/ImagePipelineObserver.swift | 79 +++++++------------ .../ImageViewExtensionsTests.swift | 6 +- .../ImagePipelineAsyncAwaitTests.swift | 36 --------- .../ImagePipelineTaskDelegateTests.swift | 17 ++-- Tests/NukeTests/ImagePrefetcherTests.swift | 12 +-- Tests/NukeUITests/FetchImageTests.swift | 2 +- 9 files changed, 43 insertions(+), 151 deletions(-) diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift b/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift index 3e6f4d99d..9d6601de9 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift @@ -68,21 +68,6 @@ public protocol ImagePipelineDelegate: AnyObject, Sendable { /// Gets called when the task receives an event. func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline) - - /// - warning: Soft-deprecated in Nuke 12.7. - func imageTaskDidStart(_ task: ImageTask, pipeline: ImagePipeline) - - /// - warning: Soft-deprecated in Nuke 12.7. - func imageTask(_ task: ImageTask, didUpdateProgress progress: ImageTask.Progress, pipeline: ImagePipeline) - - /// - warning: Soft-deprecated in Nuke 12.7. - func imageTask(_ task: ImageTask, didReceivePreview response: ImageResponse, pipeline: ImagePipeline) - - /// - warning: Soft-deprecated in Nuke 12.7. - func imageTaskDidCancel(_ task: ImageTask, pipeline: ImagePipeline) - - /// - warning: Soft-deprecated in Nuke 12.7. - func imageTask(_ task: ImageTask, didCompleteWithResult result: Result, pipeline: ImagePipeline) } extension ImagePipelineDelegate { @@ -127,16 +112,6 @@ extension ImagePipelineDelegate { public func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline) {} public func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, 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/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 86e1bb39e..c2a13ba01 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -300,7 +300,6 @@ public final class ImagePipeline { tasks[task] = worker.subscribe(priority: task.priority.taskPriority, subscriber: task) { [weak task] in task?.process($0) } - delegate.imageTaskDidStart(task, pipeline: self) onTaskStarted?(task) } @@ -319,19 +318,8 @@ public final class ImagePipeline { tasks[task] = nil default: break } - if !isDataTask { delegate.imageTask(task, didReceiveEvent: event, pipeline: self) - switch event { - case .progress(let progress): - delegate.imageTask(task, didUpdateProgress: progress, pipeline: self) - case .preview(let response): - delegate.imageTask(task, didReceivePreview: response, pipeline: self) - case .cancelled: - delegate.imageTaskDidCancel(task, pipeline: self) - case .finished(let result): - delegate.imageTask(task, didCompleteWithResult: result, pipeline: self) - } } } diff --git a/Sources/NukeVideo/VideoPlayerView.swift b/Sources/NukeVideo/VideoPlayerView.swift index 9120033ec..28d94864b 100644 --- a/Sources/NukeVideo/VideoPlayerView.swift +++ b/Sources/NukeVideo/VideoPlayerView.swift @@ -2,12 +2,7 @@ // // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). -#if swift(>=6.0) import AVKit -#else -@preconcurrency import AVKit -#endif - import Foundation #if os(macOS) diff --git a/Tests/ImagePipelineObserver.swift b/Tests/ImagePipelineObserver.swift index 09a626fe4..0b2f2f874 100644 --- a/Tests/ImagePipelineObserver.swift +++ b/Tests/ImagePipelineObserver.swift @@ -6,79 +6,56 @@ import XCTest @testable import Nuke final class ImagePipelineObserver: ImagePipelineDelegate, @unchecked Sendable { - var startedTaskCount = 0 + var createdTaskCount = 0 var cancelledTaskCount = 0 var completedTaskCount = 0 - static let didStartTask = Notification.Name("com.github.kean.Nuke.Tests.ImagePipelineObserver.DidStartTask") + static let didCreateTask = Notification.Name("com.github.kean.Nuke.Tests.ImagePipelineObserver.didCreateTask") static let didCancelTask = Notification.Name("com.github.kean.Nuke.Tests.ImagePipelineObserver.DidCancelTask") static let didCompleteTask = Notification.Name("com.github.kean.Nuke.Tests.ImagePipelineObserver.DidFinishTask") static let taskKey = "taskKey" static let resultKey = "resultKey" - var events = [ImageTaskEvent]() + var events = [ImageTask.Event]() private let lock = NSLock() - private func append(_ event: ImageTaskEvent) { + private func append(_ event: ImageTask.Event) { lock.lock() events.append(event) lock.unlock() } func imageTaskCreated(_ task: ImageTask, pipeline: ImagePipeline) { - append(.created) - } - - func imageTaskDidStart(_ task: ImageTask, pipeline: ImagePipeline) { - startedTaskCount += 1 - NotificationCenter.default.post(name: ImagePipelineObserver.didStartTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task]) - append(.started) - } - - func imageTaskDidCancel(_ task: ImageTask, pipeline: ImagePipeline) { - append(.cancelled) - - cancelledTaskCount += 1 - NotificationCenter.default.post(name: ImagePipelineObserver.didCancelTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task]) - } - - func imageTask(_ task: ImageTask, didUpdateProgress progress: ImageTask.Progress, pipeline: ImagePipeline) { - append(.progressUpdated(completedUnitCount: progress.completed, totalUnitCount: progress.total)) - } - - func imageTask(_ task: ImageTask, didReceivePreview response: ImageResponse, pipeline: ImagePipeline) { - append(.intermediateResponseReceived(response: response)) - } - - func imageTask(_ task: ImageTask, didCompleteWithResult result: Result, pipeline: ImagePipeline) { - append(.completed(result: result)) - - completedTaskCount += 1 - NotificationCenter.default.post(name: ImagePipelineObserver.didCompleteTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task, ImagePipelineObserver.resultKey: result]) + createdTaskCount += 1 + NotificationCenter.default.post(name: ImagePipelineObserver.didCreateTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task]) + } + + func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline) { + append(event) + + switch event { + case .finished(let result): + completedTaskCount += 1 + NotificationCenter.default.post(name: ImagePipelineObserver.didCompleteTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task, ImagePipelineObserver.resultKey: result]) + case .cancelled: + cancelledTaskCount += 1 + NotificationCenter.default.post(name: ImagePipelineObserver.didCancelTask, object: self, userInfo: [ImagePipelineObserver.taskKey: task]) + default: + break + } } } -#warning("replace with ImageTask.Event") -enum ImageTaskEvent: Equatable { - case created - case started - case cancelled - case intermediateResponseReceived(response: ImageResponse) - case progressUpdated(completedUnitCount: Int64, totalUnitCount: Int64) - case completed(result: Result) - - static func == (lhs: ImageTaskEvent, rhs: ImageTaskEvent) -> Bool { +extension ImageTask.Event: @retroactive Equatable { + public static func == (lhs: ImageTask.Event, rhs: ImageTask.Event) -> Bool { switch (lhs, rhs) { - case (.created, .created): return true - case (.started, .started): return true - case (.cancelled, .cancelled): return true - case let (.intermediateResponseReceived(lhs), .intermediateResponseReceived(rhs)): return lhs == rhs - case let (.progressUpdated(lhsTotal, lhsCompleted), .progressUpdated(rhsTotal, rhsCompleted)): - return (lhsTotal, lhsCompleted) == (rhsTotal, rhsCompleted) - case let (.completed(lhs), .completed(rhs)): return lhs == rhs - default: return false + case let (.progress(lhs), .progress(rhs)): lhs == rhs + case let (.preview(lhs), .preview(rhs)): lhs == rhs + case let (.finished(lhs), .finished(rhs)): lhs == rhs + case (.cancelled, .cancelled): true + default: false } } } diff --git a/Tests/NukeExtensionsTests/ImageViewExtensionsTests.swift b/Tests/NukeExtensionsTests/ImageViewExtensionsTests.swift index 40b671de2..3403392ab 100644 --- a/Tests/NukeExtensionsTests/ImageViewExtensionsTests.swift +++ b/Tests/NukeExtensionsTests/ImageViewExtensionsTests.swift @@ -270,7 +270,7 @@ class ImageViewExtensionsTests: XCTestCase { dataLoader.isSuspended = true // Given an image view with an associated image task - expectNotification(ImagePipelineObserver.didStartTask, object: observer) + expectNotification(ImagePipelineObserver.didCreateTask, object: observer) NukeExtensions.loadImage(with: Test.url, into: imageView) wait() @@ -287,7 +287,7 @@ class ImageViewExtensionsTests: XCTestCase { dataLoader.isSuspended = true // Given an image view with an associated image task - expectNotification(ImagePipelineObserver.didStartTask, object: observer) + expectNotification(ImagePipelineObserver.didCreateTask, object: observer) NukeExtensions.loadImage(with: Test.url, into: imageView) wait() @@ -307,7 +307,7 @@ class ImageViewExtensionsTests: XCTestCase { autoreleasepool { // Given an image view with an associated image task var imageView: _ImageView! = _ImageView() - expectNotification(ImagePipelineObserver.didStartTask, object: observer) + expectNotification(ImagePipelineObserver.didCreateTask, object: observer) NukeExtensions.loadImage(with: Test.url, into: imageView) wait() diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift index b4f95eada..1c7b575ba 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift @@ -406,39 +406,3 @@ private struct URLError: Swift.Error { case constrained } } - -#if swift(>=6.0) -extension ImageTask.Event: @retroactive Equatable { - public static func == (lhs: ImageTask.Event, rhs: ImageTask.Event) -> Bool { - switch (lhs, rhs) { - case let (.progress(lhs), .progress(rhs)): - return lhs == rhs - case let (.preview(lhs), .preview(rhs)): - return lhs == rhs - case (.cancelled, .cancelled): - return true - case let (.finished(lhs), .finished(rhs)): - return lhs == rhs - default: - return false - } - } -} -#else -extension ImageTask.Event: Equatable { - public static func == (lhs: ImageTask.Event, rhs: ImageTask.Event) -> Bool { - switch (lhs, rhs) { - case let (.progress(lhs), .progress(rhs)): - return lhs == rhs - case let (.preview(lhs), .preview(rhs)): - return lhs == rhs - case (.cancelled, .cancelled): - return true - case let (.finished(lhs), .finished(rhs)): - return lhs == rhs - default: - return false - } - } -} -#endif diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineTaskDelegateTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineTaskDelegateTests.swift index c9f5bcbfd..095b49955 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineTaskDelegateTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineTaskDelegateTests.swift @@ -22,7 +22,6 @@ class ImagePipelineTaskDelegateTests: XCTestCase { } } - #warning("remove ImageTaskEvent.created") func testStartAndCompletedEvents() throws { var result: Result? expect(pipeline).toLoadImage(with: Test.request) { result = $0 } @@ -30,10 +29,8 @@ class ImagePipelineTaskDelegateTests: XCTestCase { // Then XCTAssertEqual(delegate.events, [ - ImageTaskEvent.created, - .started, - .progressUpdated(completedUnitCount: 22789, totalUnitCount: 22789), - .completed(result: try XCTUnwrap(result)) + .progress(.init(completed: 22789, total: 22789)), + .finished(try XCTUnwrap(result)) ]) } @@ -49,11 +46,9 @@ class ImagePipelineTaskDelegateTests: XCTestCase { // Then XCTAssertEqual(delegate.events, [ - ImageTaskEvent.created, - .started, - .progressUpdated(completedUnitCount: 10, totalUnitCount: 20), - .progressUpdated(completedUnitCount: 20, totalUnitCount: 20), - .completed(result: try XCTUnwrap(result)) + .progress(.init(completed: 10, total: 20)), + .progress(.init(completed: 20, total: 20)), + .finished(try XCTUnwrap(result)) ]) } @@ -72,8 +67,6 @@ class ImagePipelineTaskDelegateTests: XCTestCase { // Then XCTAssertEqual(delegate.events, [ - ImageTaskEvent.created, - .started, .cancelled ]) } diff --git a/Tests/NukeTests/ImagePrefetcherTests.swift b/Tests/NukeTests/ImagePrefetcherTests.swift index b0e2c177f..57d058068 100644 --- a/Tests/NukeTests/ImagePrefetcherTests.swift +++ b/Tests/NukeTests/ImagePrefetcherTests.swift @@ -53,7 +53,7 @@ final class ImagePrefetcherTests: XCTestCase { // THEN XCTAssertEqual(dataLoader.createdTaskCount, 1) - XCTAssertEqual(observer.startedTaskCount, 2) + XCTAssertEqual(observer.createdTaskCount, 2) } // MARK: Start Prefetching @@ -85,7 +85,7 @@ final class ImagePrefetcherTests: XCTestCase { wait() // THEN only one task is started - XCTAssertEqual(observer.startedTaskCount, 1) + XCTAssertEqual(observer.createdTaskCount, 1) } func testWhenImageIsInMemoryCacheNoTaskStarted() { @@ -99,7 +99,7 @@ final class ImagePrefetcherTests: XCTestCase { pipeline.queue.sync {} // THEN - XCTAssertEqual(observer.startedTaskCount, 0) + XCTAssertEqual(observer.createdTaskCount, 0) } // MARK: Stop Prefetching @@ -109,7 +109,7 @@ final class ImagePrefetcherTests: XCTestCase { // WHEN let url = Test.url - expectNotification(ImagePipelineObserver.didStartTask, object: observer) + expectNotification(ImagePipelineObserver.didCreateTask, object: observer) prefetcher.startPrefetching(with: [url]) wait() @@ -156,7 +156,7 @@ final class ImagePrefetcherTests: XCTestCase { wait() // THEN - XCTAssertEqual(observer.startedTaskCount, 0) + XCTAssertEqual(observer.createdTaskCount, 0) } // MARK: Priority @@ -273,7 +273,7 @@ final class ImagePrefetcherTests: XCTestCase { pipeline.configuration.dataLoadingQueue.isSuspended = true let request = Test.request - expectNotification(ImagePipelineObserver.didStartTask, object: observer) + expectNotification(ImagePipelineObserver.didCreateTask, object: observer) prefetcher.startPrefetching(with: [request]) wait() diff --git a/Tests/NukeUITests/FetchImageTests.swift b/Tests/NukeUITests/FetchImageTests.swift index a0264a89d..cab3c7bb6 100644 --- a/Tests/NukeUITests/FetchImageTests.swift +++ b/Tests/NukeUITests/FetchImageTests.swift @@ -156,7 +156,7 @@ class FetchImageTests: XCTestCase { // gets deallocated immediately. autoreleasepool { // Given an image view with an associated image task - expectNotification(ImagePipelineObserver.didStartTask, object: observer) + expectNotification(ImagePipelineObserver.didCreateTask, object: observer) image.load(pipeline.imagePublisher(with: Test.request)) wait() From 45c7fd685569f22d8ff2d8ca8d87f48aefd144b3 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 17:37:39 -0400 Subject: [PATCH 24/73] Rename ImagePipelineDelegate to ImagePipeline.Delegate --- .../Pipeline/ImagePipeline+Delegate.swift | 130 +++++++++--------- Sources/Nuke/Pipeline/ImagePipeline.swift | 6 +- Tests/ImagePipelineObserver.swift | 2 +- .../DocumentationTests.swift | 2 +- .../ImagePipelineDelegateTests.swift | 2 +- 5 files changed, 74 insertions(+), 68 deletions(-) diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift b/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift index 9d6601de9..dd2065da8 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift @@ -8,69 +8,71 @@ import Foundation /// /// - 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: ImageTask - - /// 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 receives an event. - func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline) +extension ImagePipeline { + public protocol Delegate: 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: ImageTask + + /// 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 receives an event. + func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline) + } } -extension ImagePipelineDelegate { +extension ImagePipeline.Delegate { public func imageCache(for request: ImageRequest, pipeline: ImagePipeline) -> (any ImageCaching)? { pipeline.configuration.imageCache } @@ -114,4 +116,8 @@ extension ImagePipelineDelegate { public func imageTask(_ task: ImageTask, didReceiveEvent event: ImageTask.Event, pipeline: ImagePipeline) {} } -final class ImagePipelineDefaultDelegate: ImagePipelineDelegate {} +final class ImagePipelineDefaultDelegate: ImagePipeline.Delegate {} + +// Deprecated in Nuke 13.0 +@available(*, deprecated, renamed: "ImagePipeline.Delegate", message: "") +public typealias ImagePipelineDelegate = ImagePipeline.Delegate diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index c2a13ba01..46ccdbcb1 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -30,7 +30,7 @@ public final class ImagePipeline { /// Provides access to the underlying caching subsystems. public nonisolated var cache: ImagePipeline.Cache { .init(pipeline: self) } - let delegate: any ImagePipelineDelegate + let delegate: any ImagePipeline.Delegate private var tasks = [ImageTask: TaskSubscription]() @@ -57,7 +57,7 @@ public final class ImagePipeline { /// - parameters: /// - configuration: The pipeline configuration. /// - delegate: Provides more ways to customize the pipeline behavior on per-request basis. - public nonisolated init(configuration: Configuration = Configuration(), delegate: (any ImagePipelineDelegate)? = nil) { + public nonisolated init(configuration: Configuration = Configuration(), delegate: (any ImagePipeline.Delegate)? = nil) { self.configuration = configuration self.rateLimiter = configuration.isRateLimiterEnabled ? RateLimiter() : nil self.delegate = delegate ?? ImagePipelineDefaultDelegate() @@ -86,7 +86,7 @@ public final class ImagePipeline { /// - parameters: /// - configuration: The pipeline configuration. /// - delegate: Provides more ways to customize the pipeline behavior on per-request basis. - public nonisolated convenience init(delegate: (any ImagePipelineDelegate)? = nil, _ configure: (inout ImagePipeline.Configuration) -> Void) { + public nonisolated convenience init(delegate: (any ImagePipeline.Delegate)? = nil, _ configure: (inout ImagePipeline.Configuration) -> Void) { var configuration = ImagePipeline.Configuration() configure(&configuration) self.init(configuration: configuration, delegate: delegate) diff --git a/Tests/ImagePipelineObserver.swift b/Tests/ImagePipelineObserver.swift index 0b2f2f874..27477f998 100644 --- a/Tests/ImagePipelineObserver.swift +++ b/Tests/ImagePipelineObserver.swift @@ -5,7 +5,7 @@ import XCTest @testable import Nuke -final class ImagePipelineObserver: ImagePipelineDelegate, @unchecked Sendable { +final class ImagePipelineObserver: ImagePipeline.Delegate, @unchecked Sendable { var createdTaskCount = 0 var cancelledTaskCount = 0 var completedTaskCount = 0 diff --git a/Tests/NukeTests/ImagePipelineTests/DocumentationTests.swift b/Tests/NukeTests/ImagePipelineTests/DocumentationTests.swift index a7e265522..583286504 100644 --- a/Tests/NukeTests/ImagePipelineTests/DocumentationTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/DocumentationTests.swift @@ -117,7 +117,7 @@ private func checkAccessCachedImages07() { _ = pipeline.cache.makeDataCacheKey(for: request) } -private final class CheckAccessCachedImages08: ImagePipelineDelegate { +private final class CheckAccessCachedImages08: ImagePipeline.Delegate { func cacheKey(for request: ImageRequest, pipeline: ImagePipeline) -> String? { request.userInfo["imageId"] as? String } diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineDelegateTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineDelegateTests.swift index 244561ed8..e27bcaaf6 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineDelegateTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineDelegateTests.swift @@ -89,7 +89,7 @@ class ImagePipelineDelegateTests: XCTestCase { } } -private final class MockImagePipelineDelegate: ImagePipelineDelegate, @unchecked Sendable { +private final class MockImagePipelineDelegate: ImagePipeline.Delegate, @unchecked Sendable { var isCacheEnabled = true func cacheKey(for request: ImageRequest, pipeline: ImagePipeline) -> String? { From a2bd2f7e56cb010eb61653dc4bac1e273b571bd5 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 17:43:17 -0400 Subject: [PATCH 25/73] Move Combine extensions --- Nuke.xcodeproj/project.pbxproj | 20 +++++++++---------- Sources/Nuke/Pipeline/ImagePipeline.swift | 13 ------------ .../ImagePipeline+Combine.swift} | 17 ++++++++++++++-- Tests/NukeExtensions.swift | 3 +++ .../ImagePipelinePublisherTests.swift | 0 .../ImagePublisherTests.swift | 0 .../ImageViewExtensionsTests.swift | 3 ++- .../ImageViewIntegrationTests.swift | 3 ++- .../ImageViewLoadingOptionsTests.swift | 3 ++- 9 files changed, 34 insertions(+), 28 deletions(-) rename Sources/{Nuke/Internal/ImagePublisher.swift => NukeExtensions/ImagePipeline+Combine.swift} (81%) rename Tests/{NukeTests/ImagePipelineTests => NukeExtensionsTests}/ImagePipelinePublisherTests.swift (100%) rename Tests/{NukeTests => NukeExtensionsTests}/ImagePublisherTests.swift (100%) diff --git a/Nuke.xcodeproj/project.pbxproj b/Nuke.xcodeproj/project.pbxproj index 3713754dc..721044aef 100644 --- a/Nuke.xcodeproj/project.pbxproj +++ b/Nuke.xcodeproj/project.pbxproj @@ -19,10 +19,13 @@ 0C0FD6041CA47FE1002A78FB /* ImageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0FD5D91CA47FE1002A78FB /* ImageRequest.swift */; }; 0C1453A02657EFA7005E24B3 /* ImagePipelineObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C14539F2657EFA7005E24B3 /* ImagePipelineObserver.swift */; }; 0C1453A12657EFA7005E24B3 /* ImagePipelineObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C14539F2657EFA7005E24B3 /* ImagePipelineObserver.swift */; }; + 0C16C85F2C7150C800B2A560 /* ImagePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C88C578263DAF1E0061A008 /* ImagePublisherTests.swift */; }; 0C179C7B2283597F008AB488 /* ImageEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C179C7A2283597F008AB488 /* ImageEncoding.swift */; }; 0C1B9880294E28D800C09310 /* Nuke.docc in Sources */ = {isa = PBXBuildFile; fileRef = 0C1B987F294E28D800C09310 /* Nuke.docc */; }; 0C1C201D29ABBF19004B38FD /* Nuke.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; }; 0C1C201E29ABBF19004B38FD /* Nuke.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 0C1D2D5E2C714FF900BB81B3 /* ImagePipeline+Combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1D2D5D2C714FF900BB81B3 /* ImagePipeline+Combine.swift */; }; + 0C1D2D5F2C71505900BB81B3 /* ImagePipelinePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE6202426543EC700AAB8C3 /* ImagePipelinePublisherTests.swift */; }; 0C1E620B1D6F817700AD5CF5 /* ImageRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1E620A1D6F817700AD5CF5 /* ImageRequestTests.swift */; }; 0C1ECA421D526461009063A9 /* ImageCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C06871BCA888800089D7F /* ImageCacheTests.swift */; }; 0C222DE3294E2DEA00012288 /* NukeUI.docc in Sources */ = {isa = PBXBuildFile; fileRef = 0C222DE2294E2DEA00012288 /* NukeUI.docc */; }; @@ -142,7 +145,6 @@ 0C8684FF20BDD578009FF7CC /* ImagePipelineProgressiveDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2A8CFA20970D8D0013FD65 /* ImagePipelineProgressiveDecodingTests.swift */; }; 0C86AB6A228B3B5100A81BA1 /* ImageTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C86AB69228B3B5100A81BA1 /* ImageTask.swift */; }; 0C880532242E7B1500F8C5B3 /* ImagePipelineDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C880531242E7B1500F8C5B3 /* ImagePipelineDecodingTests.swift */; }; - 0C88C579263DAF1E0061A008 /* ImagePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C88C578263DAF1E0061A008 /* ImagePublisherTests.swift */; }; 0C8D7BD31D9DBF1600D12EB7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8D7BD21D9DBF1600D12EB7 /* AppDelegate.swift */; }; 0C8D7BD51D9DBF1600D12EB7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8D7BD41D9DBF1600D12EB7 /* ViewController.swift */; }; 0C8D7BD81D9DBF1600D12EB7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C8D7BD61D9DBF1600D12EB7 /* Main.storyboard */; }; @@ -186,7 +188,6 @@ 0CA4ECCD26E68FA100BAC8E5 /* DataLoading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA4ECCC26E68FA100BAC8E5 /* DataLoading.swift */; }; 0CA4ECD026E68FC000BAC8E5 /* DataCaching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA4ECCF26E68FC000BAC8E5 /* DataCaching.swift */; }; 0CA4ECD326E68FDC00BAC8E5 /* ImageCaching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA4ECD226E68FDC00BAC8E5 /* ImageCaching.swift */; }; - 0CA5D954263CCEA500E08E17 /* ImagePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA5D953263CCEA500E08E17 /* ImagePublisher.swift */; }; 0CA8D8ED2958DA3700EDAA2C /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA8D8EC2958DA3700EDAA2C /* Mutex.swift */; }; 0CAAB0101E45D6DA00924450 /* NukeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAAB00F1E45D6DA00924450 /* NukeExtensions.swift */; }; 0CAAB0131E45D6DA00924450 /* NukeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAAB00F1E45D6DA00924450 /* NukeExtensions.swift */; }; @@ -248,7 +249,6 @@ 0CE5F6832156386B0046609F /* ResumableDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE5F681215638300046609F /* ResumableDataTests.swift */; }; 0CE6202126542F7200AAB8C3 /* DataPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE6202026542F7200AAB8C3 /* DataPublisher.swift */; }; 0CE6202326543B6A00AAB8C3 /* TaskFetchWithPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE6202226543B6A00AAB8C3 /* TaskFetchWithPublisher.swift */; }; - 0CE6202526543EC700AAB8C3 /* ImagePipelinePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE6202426543EC700AAB8C3 /* ImagePipelinePublisherTests.swift */; }; 0CE6202726546FD100AAB8C3 /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE6202626546FD100AAB8C3 /* CombineExtensions.swift */; }; 0CE745751D4767B900123F65 /* MockImageDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE745741D4767B900123F65 /* MockImageDecoder.swift */; }; 0CF1754C22913F9800A8946E /* ImagePipeline+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF1754B22913F9800A8946E /* ImagePipeline+Configuration.swift */; }; @@ -355,6 +355,7 @@ 0C179C772282AC50008AB488 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = ""; }; 0C179C7A2283597F008AB488 /* ImageEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageEncoding.swift; sourceTree = ""; }; 0C1B987F294E28D800C09310 /* Nuke.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Nuke.docc; sourceTree = ""; }; + 0C1D2D5D2C714FF900BB81B3 /* ImagePipeline+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImagePipeline+Combine.swift"; sourceTree = ""; }; 0C1E620A1D6F817700AD5CF5 /* ImageRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageRequestTests.swift; sourceTree = ""; }; 0C222DE2294E2DEA00012288 /* NukeUI.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = NukeUI.docc; sourceTree = ""; }; 0C222DE4294E2E0200012288 /* NukeExtensions.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = NukeExtensions.docc; sourceTree = ""; }; @@ -475,7 +476,6 @@ 0CA4ECCC26E68FA100BAC8E5 /* DataLoading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataLoading.swift; sourceTree = ""; }; 0CA4ECCF26E68FC000BAC8E5 /* DataCaching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataCaching.swift; sourceTree = ""; }; 0CA4ECD226E68FDC00BAC8E5 /* ImageCaching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCaching.swift; sourceTree = ""; }; - 0CA5D953263CCEA500E08E17 /* ImagePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePublisher.swift; sourceTree = ""; }; 0CA8D8EC2958DA3700EDAA2C /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = ""; }; 0CAAB00F1E45D6DA00924450 /* NukeExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NukeExtensions.swift; sourceTree = ""; }; 0CB047992856D9AC00DF9B6D /* Cache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; @@ -680,6 +680,7 @@ children = ( 0C55FD1B28567926000FD2C9 /* ImageLoadingOptions.swift */, 0C55FD1C28567926000FD2C9 /* ImageViewExtensions.swift */, + 0C1D2D5D2C714FF900BB81B3 /* ImagePipeline+Combine.swift */, ); path = NukeExtensions; sourceTree = ""; @@ -699,7 +700,6 @@ 0C4AF1E91FE8551D002F86CB /* LinkedListTest.swift */, 0CB2EFD52110F52C00F7C63F /* RateLimiterTests.swift */, 0C0F7BF02287F6EE0034E656 /* TaskTests.swift */, - 0C88C578263DAF1E0061A008 /* ImagePublisherTests.swift */, 0C472F802654AA46007FC0F0 /* DeprecationTests.swift */, 0C91B0E82438E245007F9100 /* ImagePipelineTests */, 0C91B0EA2438E269007F9100 /* ImageProcessorsTests */, @@ -712,6 +712,8 @@ children = ( 0CCBB52F217D0B6A0026F552 /* ImageViewIntegrationTests.swift */, 0C94466D1D47EC0E006DB314 /* ImageViewExtensionsTests.swift */, + 0CE6202426543EC700AAB8C3 /* ImagePipelinePublisherTests.swift */, + 0C88C578263DAF1E0061A008 /* ImagePublisherTests.swift */, 0CB6449B28567E5400916267 /* ImageViewLoadingOptionsTests.swift */, 0CB6449928567DE000916267 /* NukeExtensionsTestsHelpers.swift */, 0CB644AA28567EEA00916267 /* ImageViewExtensionsProgressiveDecodingTests.swift */, @@ -830,7 +832,6 @@ 0CD37C9925BA36D5006C2C36 /* ImagePipelineLoadDataTests.swift */, 0C53C8AE263C7B1700E62D03 /* ImagePipelineDelegateTests.swift */, 0CA3BA62285C11EA0079A444 /* ImagePipelineTaskDelegateTests.swift */, - 0CE6202426543EC700AAB8C3 /* ImagePipelinePublisherTests.swift */, 0C5D5A9C2724773A0056B95B /* ImagePipelineAsyncAwaitTests.swift */, 0C78A2A8263F560A0051E0FF /* ImagePipelineCacheTests.swift */, 0C967EB228688B3F0050E083 /* DocumentationTests.swift */, @@ -997,7 +998,6 @@ 0C7150081FC9724C00B880AC /* Extensions.swift */, 0CA8D8EC2958DA3700EDAA2C /* Mutex.swift */, 0CE6202026542F7200AAB8C3 /* DataPublisher.swift */, - 0CA5D953263CCEA500E08E17 /* ImagePublisher.swift */, 0C472F822654AD69007FC0F0 /* ImageRequestKeys.swift */, ); path = Internal; @@ -1571,6 +1571,7 @@ 0C55FD1F28567926000FD2C9 /* ImageViewExtensions.swift in Sources */, 0C222DE5294E2E0300012288 /* NukeExtensions.docc in Sources */, 0C55FD1E28567926000FD2C9 /* ImageLoadingOptions.swift in Sources */, + 0C1D2D5E2C714FF900BB81B3 /* ImagePipeline+Combine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1578,6 +1579,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0C16C85F2C7150C800B2A560 /* ImagePublisherTests.swift in Sources */, 0CB6449828567DCA00916267 /* CombineExtensions.swift in Sources */, 0CB6449728567DCA00916267 /* NukeExtensions.swift in Sources */, 0C55FD2728567C12000FD2C9 /* ImageViewExtensionsTests.swift in Sources */, @@ -1596,6 +1598,7 @@ 0CB6448C28567DC300916267 /* MockDataCache.swift in Sources */, 0CB6449628567DCA00916267 /* XCTestCaseExtensions.swift in Sources */, 0CB6448F28567DC300916267 /* MockImageEncoder.swift in Sources */, + 0C1D2D5F2C71505900BB81B3 /* ImagePipelinePublisherTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1625,7 +1628,6 @@ 0CAAB0101E45D6DA00924450 /* NukeExtensions.swift in Sources */, 0CE745751D4767B900123F65 /* MockImageDecoder.swift in Sources */, 0C70D9782089017500A49DAC /* ImageDecoderTests.swift in Sources */, - 0C88C579263DAF1E0061A008 /* ImagePublisherTests.swift in Sources */, 0C7082612640521900C62638 /* MockImageEncoder.swift in Sources */, 0CE6202726546FD100AAB8C3 /* CombineExtensions.swift in Sources */, 0C6B5BE1257010D300D763F2 /* ImagePipelineFormatsTests.swift in Sources */, @@ -1662,7 +1664,6 @@ 0CA3BA63285C11EA0079A444 /* ImagePipelineTaskDelegateTests.swift in Sources */, 0C6D0A8C20E57C810037B68F /* ImagePipelineDataCacheTests.swift in Sources */, 0C68F609208A1F40007DC696 /* ImageDecoderRegistryTests.swift in Sources */, - 0CE6202526543EC700AAB8C3 /* ImagePipelinePublisherTests.swift in Sources */, 0C91B0EE2438E307007F9100 /* CircleTests.swift in Sources */, 0CC6271525BDF7A100466F04 /* ImagePipelineImageCacheTests.swift in Sources */, ); @@ -1712,7 +1713,6 @@ 0CC36A2C25B8BC6300811018 /* LinkedList.swift in Sources */, 0C179C7B2283597F008AB488 /* ImageEncoding.swift in Sources */, 0CB4030125B6639200F5A241 /* TaskLoadImage.swift in Sources */, - 0CA5D954263CCEA500E08E17 /* ImagePublisher.swift in Sources */, 0CA4ECCD26E68FA100BAC8E5 /* DataLoading.swift in Sources */, 0CA4ECAF26E683FD00BAC8E5 /* ImageEncoders+Default.swift in Sources */, 0CC36A2525B8BC4900811018 /* Operation.swift in Sources */, diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 46ccdbcb1..eb7de2523 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -3,7 +3,6 @@ // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). import Foundation -import Combine #if canImport(UIKit) import UIKit @@ -271,18 +270,6 @@ public final class ImagePipeline { } } - // MARK: - Loading Images (Combine) - - /// Returns a publisher which starts a new ``ImageTask`` when a subscriber is added. - public nonisolated 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 nonisolated func imagePublisher(with request: ImageRequest) -> AnyPublisher { - ImagePublisher(request: request, pipeline: self).eraseToAnyPublisher() - } - // MARK: - ImageTask (Internal) private nonisolated func makeStartedImageTask(with request: ImageRequest, isDataTask: Bool = false, onEvent: ((ImageTask.Event, ImageTask) -> Void)? = nil) -> ImageTask { diff --git a/Sources/Nuke/Internal/ImagePublisher.swift b/Sources/NukeExtensions/ImagePipeline+Combine.swift similarity index 81% rename from Sources/Nuke/Internal/ImagePublisher.swift rename to Sources/NukeExtensions/ImagePipeline+Combine.swift index 985fbc992..aeaa886f9 100644 --- a/Sources/Nuke/Internal/ImagePublisher.swift +++ b/Sources/NukeExtensions/ImagePipeline+Combine.swift @@ -1,9 +1,22 @@ // The MIT License (MIT) // -// Copyright (c) 2020-2024 Alexander Grebenyuk (github.com/kean). +// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). -import Foundation import Combine +import Foundation +import Nuke + +extension ImagePipeline { + /// Returns a publisher which starts a new ``ImageTask`` when a subscriber is added. + public nonisolated 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 nonisolated func imagePublisher(with request: ImageRequest) -> AnyPublisher { + ImagePublisher(request: request, pipeline: self).eraseToAnyPublisher() + } +} /// A publisher that starts a new `ImageTask` when a subscriber is added. /// diff --git a/Tests/NukeExtensions.swift b/Tests/NukeExtensions.swift index 871400dc8..873c4a894 100644 --- a/Tests/NukeExtensions.swift +++ b/Tests/NukeExtensions.swift @@ -37,13 +37,16 @@ extension ImagePipeline { } extension ImagePipeline { + @MainActor private static var stack = [ImagePipeline]() + @MainActor static func pushShared(_ shared: ImagePipeline) { stack.append(ImagePipeline.shared) ImagePipeline.shared = shared } + @MainActor static func popShared() { ImagePipeline.shared = stack.removeLast() } diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelinePublisherTests.swift b/Tests/NukeExtensionsTests/ImagePipelinePublisherTests.swift similarity index 100% rename from Tests/NukeTests/ImagePipelineTests/ImagePipelinePublisherTests.swift rename to Tests/NukeExtensionsTests/ImagePipelinePublisherTests.swift diff --git a/Tests/NukeTests/ImagePublisherTests.swift b/Tests/NukeExtensionsTests/ImagePublisherTests.swift similarity index 100% rename from Tests/NukeTests/ImagePublisherTests.swift rename to Tests/NukeExtensionsTests/ImagePublisherTests.swift diff --git a/Tests/NukeExtensionsTests/ImageViewExtensionsTests.swift b/Tests/NukeExtensionsTests/ImageViewExtensionsTests.swift index 3403392ab..d30a872d2 100644 --- a/Tests/NukeExtensionsTests/ImageViewExtensionsTests.swift +++ b/Tests/NukeExtensionsTests/ImageViewExtensionsTests.swift @@ -35,7 +35,8 @@ class ImageViewExtensionsTests: XCTestCase { imageView = _ImageView() } - + + @MainActor override func tearDown() { super.tearDown() diff --git a/Tests/NukeExtensionsTests/ImageViewIntegrationTests.swift b/Tests/NukeExtensionsTests/ImageViewIntegrationTests.swift index 38b64892d..795e72830 100644 --- a/Tests/NukeExtensionsTests/ImageViewIntegrationTests.swift +++ b/Tests/NukeExtensionsTests/ImageViewIntegrationTests.swift @@ -26,7 +26,8 @@ class ImageViewIntegrationTests: XCTestCase { imageView = _ImageView() } - + + @MainActor override func tearDown() { super.tearDown() diff --git a/Tests/NukeExtensionsTests/ImageViewLoadingOptionsTests.swift b/Tests/NukeExtensionsTests/ImageViewLoadingOptionsTests.swift index 2cce4be80..81b76bbc9 100644 --- a/Tests/NukeExtensionsTests/ImageViewLoadingOptionsTests.swift +++ b/Tests/NukeExtensionsTests/ImageViewLoadingOptionsTests.swift @@ -28,7 +28,8 @@ class ImageViewLoadingOptionsTests: XCTestCase { imageView = _ImageView() } - + + @MainActor override func tearDown() { super.tearDown() From 351be3a42e637ab1631753bcc636dcdca6b33072 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 17:58:51 -0400 Subject: [PATCH 26/73] Remove DataPublisher support --- .../Extensions/ImageRequest-Extension.md | 1 - Nuke.xcodeproj/project.pbxproj | 16 ++--- Sources/Nuke/ImageRequest.swift | 66 ++++--------------- Sources/Nuke/Internal/DataPublisher.swift | 60 ----------------- Sources/Nuke/Internal/ImageRequestKeys.swift | 2 +- Sources/Nuke/Pipeline/ImagePipeline.swift | 4 +- ...isher.swift => TaskFetchWithClosure.swift} | 66 ++++++++++--------- .../ImagePipelinePublisherTests.swift | 50 -------------- Tests/NukeTests/DataPublisherTests.swift | 40 ----------- .../ImagePipelineTests.swift | 14 ---- 10 files changed, 55 insertions(+), 264 deletions(-) delete mode 100644 Sources/Nuke/Internal/DataPublisher.swift rename Sources/Nuke/Tasks/{TaskFetchWithPublisher.swift => TaskFetchWithClosure.swift} (52%) delete mode 100644 Tests/NukeTests/DataPublisherTests.swift diff --git a/Documentation/Nuke.docc/Extensions/ImageRequest-Extension.md b/Documentation/Nuke.docc/Extensions/ImageRequest-Extension.md index 3dcc415ce..5ce467e3e 100644 --- a/Documentation/Nuke.docc/Extensions/ImageRequest-Extension.md +++ b/Documentation/Nuke.docc/Extensions/ImageRequest-Extension.md @@ -17,7 +17,6 @@ request.processors = [.resize(width: 320)] - ``init(url:processors:priority:options:userInfo:)`` - ``init(urlRequest:processors:priority:options:userInfo:)`` - ``init(id:data:processors:priority:options:userInfo:)`` -- ``init(id:dataPublisher:processors:priority:options:userInfo:)`` - ``init(stringLiteral:)`` ### Options diff --git a/Nuke.xcodeproj/project.pbxproj b/Nuke.xcodeproj/project.pbxproj index 721044aef..6d813d42b 100644 --- a/Nuke.xcodeproj/project.pbxproj +++ b/Nuke.xcodeproj/project.pbxproj @@ -247,8 +247,7 @@ 0CE334DB2724563D0017BB8D /* ImageProcessorsProtocolExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE334DA2724563D0017BB8D /* ImageProcessorsProtocolExtensionsTests.swift */; }; 0CE3992D1D4697CE00A87D47 /* ImagePipelineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE3992C1D4697CE00A87D47 /* ImagePipelineTests.swift */; }; 0CE5F6832156386B0046609F /* ResumableDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE5F681215638300046609F /* ResumableDataTests.swift */; }; - 0CE6202126542F7200AAB8C3 /* DataPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE6202026542F7200AAB8C3 /* DataPublisher.swift */; }; - 0CE6202326543B6A00AAB8C3 /* TaskFetchWithPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE6202226543B6A00AAB8C3 /* TaskFetchWithPublisher.swift */; }; + 0CE6202326543B6A00AAB8C3 /* TaskFetchWithClosure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE6202226543B6A00AAB8C3 /* TaskFetchWithClosure.swift */; }; 0CE6202726546FD100AAB8C3 /* CombineExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE6202626546FD100AAB8C3 /* CombineExtensions.swift */; }; 0CE745751D4767B900123F65 /* MockImageDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE745741D4767B900123F65 /* MockImageDecoder.swift */; }; 0CF1754C22913F9800A8946E /* ImagePipeline+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF1754B22913F9800A8946E /* ImagePipeline+Configuration.swift */; }; @@ -261,7 +260,6 @@ 0CF5456B25B39A0E00B45F1E /* right-orientation.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 0CF5456A25B39A0E00B45F1E /* right-orientation.jpeg */; }; 0CF58FF726DAAC3800D2650D /* ImageDownsampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CF58FF626DAAC3800D2650D /* ImageDownsampleTests.swift */; }; 2DFD93B0233A6AB300D84DB9 /* ImagePipelineProcessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFD93AF233A6AB300D84DB9 /* ImagePipelineProcessorTests.swift */; }; - 4480674C2A448C9F00DE7CF8 /* DataPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4480674B2A448C9F00DE7CF8 /* DataPublisherTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -515,8 +513,7 @@ 0CE5F681215638300046609F /* ResumableDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumableDataTests.swift; sourceTree = ""; }; 0CE5F78720A22ABF00BC3283 /* Nuke 6 Migration Guide.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Nuke 6 Migration Guide.md"; sourceTree = ""; }; 0CE5F78820A22ABF00BC3283 /* Nuke 7 Migration Guide.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Nuke 7 Migration Guide.md"; sourceTree = ""; }; - 0CE6202026542F7200AAB8C3 /* DataPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataPublisher.swift; sourceTree = ""; }; - 0CE6202226543B6A00AAB8C3 /* TaskFetchWithPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskFetchWithPublisher.swift; sourceTree = ""; }; + 0CE6202226543B6A00AAB8C3 /* TaskFetchWithClosure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskFetchWithClosure.swift; sourceTree = ""; }; 0CE6202426543EC700AAB8C3 /* ImagePipelinePublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePipelinePublisherTests.swift; sourceTree = ""; }; 0CE6202626546FD100AAB8C3 /* CombineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = ""; }; 0CE745741D4767B900123F65 /* MockImageDecoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockImageDecoder.swift; sourceTree = ""; }; @@ -526,7 +523,6 @@ 0CF5456A25B39A0E00B45F1E /* right-orientation.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "right-orientation.jpeg"; sourceTree = ""; }; 0CF58FF626DAAC3800D2650D /* ImageDownsampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownsampleTests.swift; sourceTree = ""; }; 2DFD93AF233A6AB300D84DB9 /* ImagePipelineProcessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePipelineProcessorTests.swift; sourceTree = ""; }; - 4480674B2A448C9F00DE7CF8 /* DataPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataPublisherTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -689,7 +685,6 @@ isa = PBXGroup; children = ( 0C1E620A1D6F817700AD5CF5 /* ImageRequestTests.swift */, - 4480674B2A448C9F00DE7CF8 /* DataPublisherTests.swift */, 0C7C06871BCA888800089D7F /* ImageCacheTests.swift */, 0C70D9772089017500A49DAC /* ImageDecoderTests.swift */, 0C68F608208A1F40007DC696 /* ImageDecoderRegistryTests.swift */, @@ -948,7 +943,7 @@ 0C2A368A26437BF100F1D000 /* TaskLoadData.swift */, 0CB402DA25B656D200F5A241 /* TaskFetchOriginalImage.swift */, 0CB402D425B6569700F5A241 /* TaskFetchOriginalData.swift */, - 0CE6202226543B6A00AAB8C3 /* TaskFetchWithPublisher.swift */, + 0CE6202226543B6A00AAB8C3 /* TaskFetchWithClosure.swift */, ); path = Tasks; sourceTree = ""; @@ -997,7 +992,6 @@ 0CC36A4025B8BCAC00811018 /* Log.swift */, 0C7150081FC9724C00B880AC /* Extensions.swift */, 0CA8D8EC2958DA3700EDAA2C /* Mutex.swift */, - 0CE6202026542F7200AAB8C3 /* DataPublisher.swift */, 0C472F822654AD69007FC0F0 /* ImageRequestKeys.swift */, ); path = Internal; @@ -1617,7 +1611,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4480674C2A448C9F00DE7CF8 /* DataPublisherTests.swift in Sources */, 0CC04B1C2C56AEF000F1164D /* ImagePipelineLoadDataTests.swift in Sources */, 0C75279F1D473AEF00EC6222 /* MockImageProcessor.swift in Sources */, 0CB26807208F25C2004C83F4 /* DataCacheTests.swift in Sources */, @@ -1728,12 +1721,11 @@ 0CA4ECC826E6864D00BAC8E5 /* ImageProcessors+RoundedCorners.swift in Sources */, 0CB402DB25B656D200F5A241 /* TaskFetchOriginalImage.swift in Sources */, 0C472F842654AD88007FC0F0 /* ImageRequestKeys.swift in Sources */, - 0CE6202126542F7200AAB8C3 /* DataPublisher.swift in Sources */, 0CB0479A2856D9AC00DF9B6D /* Cache.swift in Sources */, 0CA4ECB626E6846800BAC8E5 /* ImageProcessors+Resize.swift in Sources */, 0C1B9880294E28D800C09310 /* Nuke.docc in Sources */, 0CC36A3325B8BC7900811018 /* ResumableData.swift in Sources */, - 0CE6202326543B6A00AAB8C3 /* TaskFetchWithPublisher.swift in Sources */, + 0CE6202326543B6A00AAB8C3 /* TaskFetchWithClosure.swift in Sources */, 0CA4ECC426E685F500BAC8E5 /* ImageProcessors+GaussianBlur.swift in Sources */, 0CA4EC9B26E67D3000BAC8E5 /* ImageDecoders+Empty.swift in Sources */, 0CB26802208F2565004C83F4 /* DataCache.swift in Sources */, diff --git a/Sources/Nuke/ImageRequest.swift b/Sources/Nuke/ImageRequest.swift index 96d20870f..cf67aa992 100644 --- a/Sources/Nuke/ImageRequest.swift +++ b/Sources/Nuke/ImageRequest.swift @@ -69,7 +69,7 @@ public struct ImageRequest: CustomStringConvertible, Sendable, ExpressibleByStri 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 + case .closure: return nil } } @@ -80,7 +80,7 @@ public struct ImageRequest: CustomStringConvertible, Sendable, ExpressibleByStri switch ref.resource { case .url(let url): return url case .urlRequest(let request): return request.url - case .publisher: return nil + case .closure: return nil } } @@ -202,51 +202,7 @@ public struct ImageRequest: CustomStringConvertible, Sendable, ExpressibleByStri // 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)), + resource: .closure(data, id: id), processors: processors, priority: priority, options: options, @@ -470,8 +426,8 @@ public struct ImageRequest: CustomStringConvertible, Sendable, ExpressibleByStri (ref.userInfo?[.scaleKey] as? NSNumber)?.floatValue } - var publisher: DataPublisher? { - if case .publisher(let publisher) = ref.resource { return publisher } + var closure: (@Sendable () async throws -> Data)? { + if case .closure(let closure, _) = ref.resource { return closure } return nil } } @@ -519,13 +475,13 @@ extension ImageRequest { enum Resource: CustomStringConvertible { case url(URL?) case urlRequest(URLRequest) - case publisher(DataPublisher) + case closure(@Sendable () async throws -> Data, id: String) 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)" + case .closure(_, let id): return id } } @@ -533,8 +489,14 @@ extension ImageRequest { switch self { case .url(let url): return url?.absoluteString case .urlRequest(let urlRequest): return urlRequest.url?.absoluteString - case .publisher(let publisher): return publisher.id + case .closure(_, let id): return id } } } } + +// TODO: (nuke13) remove +/// - warning: Avoid using it! +struct UncheckedSendableBox: @unchecked Sendable { + let value: Value +} diff --git a/Sources/Nuke/Internal/DataPublisher.swift b/Sources/Nuke/Internal/DataPublisher.swift deleted file mode 100644 index fc3afca54..000000000 --- a/Sources/Nuke/Internal/DataPublisher.swift +++ /dev/null @@ -1,60 +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 - let promise = UncheckedSendableBox(value: promise) - Task { - do { - let data = try await closure() - promise.value(.success(data)) - } catch { - promise.value(.failure(error)) - } - } - } - }.eraseToAnyPublisher() -} - -enum PublisherCompletion { - case finished - case failure(Error) -} - -/// - warning: Avoid using it! -struct UncheckedSendableBox: @unchecked Sendable { - let value: Value -} diff --git a/Sources/Nuke/Internal/ImageRequestKeys.swift b/Sources/Nuke/Internal/ImageRequestKeys.swift index 90046776b..213eb7a13 100644 --- a/Sources/Nuke/Internal/ImageRequestKeys.swift +++ b/Sources/Nuke/Internal/ImageRequestKeys.swift @@ -78,7 +78,7 @@ struct TaskFetchOriginalDataKey: Hashable { init(_ request: ImageRequest) { self.imageId = request.imageId switch request.resource { - case .url, .publisher: + case .url, .closure: self.cachePolicy = .useProtocolCachePolicy self.allowsCellularAccess = true case let .urlRequest(urlRequest): diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index eb7de2523..80b348c6a 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -207,7 +207,7 @@ public final class ImagePipeline { } } - // nuke-13: requires callbacks to be @MainActor @Sendable or deprecate this entire API + // TODO: (nuke13): requires callbacks to be @MainActor @Sendable or deprecate this entire API private nonisolated func dispatchCallback(to callbackQueue: DispatchQueue?, _ closure: @escaping () -> Void) { let box = UncheckedSendableBox(value: closure) (callbackQueue ?? .main).async { @@ -350,7 +350,7 @@ public final class ImagePipeline { func makeTaskFetchOriginalData(for request: ImageRequest) -> AsyncTask<(Data, URLResponse?), Error>.Publisher { tasksFetchOriginalData.publisherForKey(TaskFetchOriginalDataKey(request)) { - request.publisher == nil ? TaskFetchOriginalData(self, request) : TaskFetchWithPublisher(self, request) + request.closure == nil ? TaskFetchOriginalData(self, request) : TaskFetchWithClosure(self, request) } } } diff --git a/Sources/Nuke/Tasks/TaskFetchWithPublisher.swift b/Sources/Nuke/Tasks/TaskFetchWithClosure.swift similarity index 52% rename from Sources/Nuke/Tasks/TaskFetchWithPublisher.swift rename to Sources/Nuke/Tasks/TaskFetchWithClosure.swift index 5065604e0..d0d8dd7f1 100644 --- a/Sources/Nuke/Tasks/TaskFetchWithPublisher.swift +++ b/Sources/Nuke/Tasks/TaskFetchWithClosure.swift @@ -6,7 +6,7 @@ import Foundation /// Fetches data using the publisher provided with the request. /// Unlike `TaskFetchOriginalImageData`, there is no resumable data involved. -final class TaskFetchWithPublisher: AsyncPipelineTask<(Data, URLResponse?)> { +final class TaskFetchWithClosure: AsyncPipelineTask<(Data, URLResponse?)> { private lazy var data = Data() override func start() { @@ -32,41 +32,43 @@ final class TaskFetchWithPublisher: AsyncPipelineTask<(Data, URLResponse?)> { return finish() } - guard let publisher = request.publisher else { + guard let closure = request.closure 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 } - Task { @ImagePipelineActor in - self.dataTaskDidFinish(result) - } - }, receiveValue: { [weak self] data in - guard let self else { return } - Task { @ImagePipelineActor in - self.data.append(data) - } - }) - - onCancelled = { - finish() - cancellable.cancel() - } - } + // TODO: (nuke13) reimplement - 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)) - } +// let cancellable = closure.sink(receiveCompletion: { [weak self] result in +// finish() // Finish the operation! +// guard let self else { return } +// Task { @ImagePipelineActor in +// self.dataTaskDidFinish(result) +// } +// }, receiveValue: { [weak self] data in +// guard let self else { return } +// Task { @ImagePipelineActor in +// 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/Tests/NukeExtensionsTests/ImagePipelinePublisherTests.swift b/Tests/NukeExtensionsTests/ImagePipelinePublisherTests.swift index 2009f3aad..d8a18bb02 100644 --- a/Tests/NukeExtensionsTests/ImagePipelinePublisherTests.swift +++ b/Tests/NukeExtensionsTests/ImagePipelinePublisherTests.swift @@ -27,43 +27,6 @@ class ImagePipelinePublisherTests: XCTestCase { } } - func testLoadWithPublisher() throws { - // GIVEN - let request = ImageRequest(id: "a", dataPublisher: Just(Test.data)) - - // WHEN - let record = expect(pipeline).toLoadImage(with: request) - wait() - - // THEN - let image = try XCTUnwrap(record.image) - XCTAssertEqual(image.sizeInPixels, CGSize(width: 640, height: 480)) - } - - func testLoadWithPublisherAndApplyProcessor() throws { - // GIVEN - var request = ImageRequest(id: "a", dataPublisher: Just(Test.data)) - request.processors = [MockImageProcessor(id: "1")] - - // WHEN - let record = expect(pipeline).toLoadImage(with: request) - wait() - - // THEN - let image = try XCTUnwrap(record.image) - XCTAssertEqual(image.sizeInPixels, CGSize(width: 640, height: 480)) - XCTAssertEqual(image.nk_test_processorIDs, ["1"]) - } - - func testImageRequestWithPublisher() { - // GIVEN - let request = ImageRequest(id: "a", dataPublisher: Just(Test.data)) - - // THEN - XCTAssertNil(request.urlRequest) - XCTAssertNil(request.url) - } - func testCancellation() { // GIVEN dataLoader.isSuspended = true @@ -77,19 +40,6 @@ class ImagePipelinePublisherTests: XCTestCase { wait() } - func testDataIsStoredInDataCache() { - // GIVEN - let request = ImageRequest(id: "a", dataPublisher: Just(Test.data)) - - // WHEN - expect(pipeline).toLoadImage(with: request) - - // THEN - wait { _ in - XCTAssertFalse(self.dataCache.store.isEmpty) - } - } - func testInitWithURL() { _ = pipeline.imagePublisher(with: URL(string: "https://example.com/image.jpeg")!) } diff --git a/Tests/NukeTests/DataPublisherTests.swift b/Tests/NukeTests/DataPublisherTests.swift deleted file mode 100644 index cf460fab7..000000000 --- a/Tests/NukeTests/DataPublisherTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). - -import XCTest -import Combine -@testable import Nuke - -internal final class DataPublisherTests: XCTestCase { - - private var cancellable: (any Nuke.Cancellable)? - - func testInitNotStartsExecutionRightAway() { - let operation = MockOperation() - let publisher = DataPublisher(id: UUID().uuidString) { - await operation.execute() - } - - XCTAssertEqual(0, operation.executeCalls) - - let expOp = expectation(description: "Waits for MockOperation to complete execution") - cancellable = publisher.sink { completion in expOp.fulfill() } receiveValue: { _ in } - wait(for: [expOp], timeout: 0.2) - - XCTAssertEqual(1, operation.executeCalls) - } - - private final class MockOperation: @unchecked Sendable { - - private(set) var executeCalls = 0 - - func execute() async -> Data { - executeCalls += 1 - await Task.yield() - return Data() - } - - } - -} diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineTests.swift index 38f469b47..01e255b7e 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineTests.swift @@ -569,20 +569,6 @@ class ImagePipelineTests: XCTestCase { wait() } - func testSkipDataLoadingQueuePerRequestWithPublisher() throws { - // Given - let queue = pipeline.configuration.dataLoadingQueue - queue.isSuspended = true - - let request = ImageRequest(id: "a", dataPublisher: Just(Test.data), options: [ - .skipDataLoadingQueue - ]) - - // Then image is still loaded - expect(pipeline).toLoadImage(with: request) - wait() - } - // MARK: Misc func testLoadWithStringLiteral() async throws { From 836cf75ea8ea863a68727842cb045a1ed5fe98cd Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 17:59:47 -0400 Subject: [PATCH 27/73] Update warnings --- Sources/Nuke/Pipeline/ImagePipeline.swift | 2 +- Sources/Nuke/Prefetching/ImagePrefetcher.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 80b348c6a..893aba0f0 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -44,7 +44,7 @@ public final class ImagePipeline { let rateLimiter: RateLimiter? let id = UUID() -#warning("TODO: remove") +// TODO: remove nonisolated(unsafe) var onTaskStarted: ((ImageTask) -> Void)? // Debug purposes deinit { diff --git a/Sources/Nuke/Prefetching/ImagePrefetcher.swift b/Sources/Nuke/Prefetching/ImagePrefetcher.swift index 6f2dec9a3..df2dd25b2 100644 --- a/Sources/Nuke/Prefetching/ImagePrefetcher.swift +++ b/Sources/Nuke/Prefetching/ImagePrefetcher.swift @@ -143,7 +143,7 @@ public final class ImagePrefetcher { return } - #warning("use async/await") + // TODO: use async/await private func loadImage(task: PrefetchTask, finish: @escaping () -> Void) { task.imageTask = pipeline._loadImage(with: task.request, isDataTask: destination == .diskCache, progress: nil) { [weak self] _ in self?._remove(task) From f742a0c84e0e8c6fee964a6859936fc5fabd5c13 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 19:42:46 -0400 Subject: [PATCH 28/73] Update DataLoading protocol to use Swift Concurrency --- Sources/Nuke/Internal/Extensions.swift | 12 ---- Sources/Nuke/Loading/DataLoader.swift | 34 +++++++++-- Sources/Nuke/Loading/DataLoading.swift | 15 ++--- .../ImagePipeline+Configuration.swift | 1 + .../Nuke/Tasks/TaskFetchOriginalData.swift | 59 ++++++++++++------- Tests/MockDataLoader.swift | 37 +++++++++++- Tests/MockProgressiveDataLoader.swift | 6 +- .../ImagePipelineAsyncAwaitTests.swift | 6 +- .../ImagePipelineResumableDataTests.swift | 25 +++++++- 9 files changed, 134 insertions(+), 61 deletions(-) diff --git a/Sources/Nuke/Internal/Extensions.swift b/Sources/Nuke/Internal/Extensions.swift index 03b46dc5f..c3943676b 100644 --- a/Sources/Nuke/Internal/Extensions.swift +++ b/Sources/Nuke/Internal/Extensions.swift @@ -49,15 +49,3 @@ extension ImageRequest.Priority { } } } - -final class AnonymousCancellable: Cancellable { - let onCancel: @Sendable () -> Void - - init(_ onCancel: @Sendable @escaping () -> Void) { - self.onCancel = onCancel - } - - func cancel() { - onCancel() - } -} diff --git a/Sources/Nuke/Loading/DataLoader.swift b/Sources/Nuke/Loading/DataLoader.swift index a3076d237..1c09fc8dc 100644 --- a/Sources/Nuke/Loading/DataLoader.swift +++ b/Sources/Nuke/Loading/DataLoader.swift @@ -13,6 +13,12 @@ public final class DataLoader: DataLoading, @unchecked Sendable { /// default, `false`. public var prefersIncrementalDelivery = false + /// 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 + /// 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. @@ -90,9 +96,26 @@ public final class DataLoader: DataLoading, @unchecked Sendable { #endif }() - public func loadData(with request: URLRequest, - didReceiveData: @escaping (Data, URLResponse) -> Void, - completion: @escaping (Swift.Error?) -> Void) -> any Cancellable { + public func loadData(for request: ImageRequest) -> AsyncThrowingStream<(Data, URLResponse), Swift.Error> { + AsyncThrowingStream { continuation in + guard let urlRequest = request.urlRequest else { + return continuation.finish(throwing: URLError(.badURL)) + } + let task = loadData(with: urlRequest) { data, response in + continuation.yield((data, response)) + } completion: { error in + continuation.finish(throwing: error) + } + continuation.onTermination = { reason in + switch reason { + case .cancelled: task.cancel() + default: break + } + } + } + } + + private func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Swift.Error?) -> Void) -> URLSessionTask { let task = session.dataTask(with: request) if #available(iOS 14.5, tvOS 14.5, watchOS 7.4, macOS 11.3, *) { task.prefersIncrementalDelivery = prefersIncrementalDelivery @@ -130,13 +153,13 @@ private final class _DataLoader: NSObject, URLSessionDataDelegate, @unchecked Se func loadData(with task: URLSessionDataTask, session: URLSession, didReceiveData: @escaping (Data, URLResponse) -> Void, - completion: @escaping (Error?) -> Void) -> any Cancellable { + completion: @escaping (Error?) -> Void) -> URLSessionTask { let handler = _Handler(didReceiveData: didReceiveData, completion: completion) session.delegateQueue.addOperation { // `URLSession` is configured to use this same queue self.handlers[task] = handler } task.resume() - return AnonymousCancellable { task.cancel() } + return task } // MARK: URLSessionDelegate @@ -223,6 +246,7 @@ private final class _DataLoader: NSObject, URLSessionDataDelegate, @unchecked Se private final class _Handler: @unchecked Sendable { let didReceiveData: (Data, URLResponse) -> Void let completion: (Error?) -> Void + var resumableData: Data? init(didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) { self.didReceiveData = didReceiveData diff --git a/Sources/Nuke/Loading/DataLoading.swift b/Sources/Nuke/Loading/DataLoading.swift index e4da75e17..c7700a47a 100644 --- a/Sources/Nuke/Loading/DataLoading.swift +++ b/Sources/Nuke/Loading/DataLoading.swift @@ -6,16 +6,9 @@ import Foundation /// Fetches original image data. public protocol DataLoading: Sendable { - /// - parameter didReceiveData: Can be called multiple times if streaming + /// Returns data for the given request. + /// + /// - returns: Sequence that can be called more than once 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() + func loadData(for request: ImageRequest) -> AsyncThrowingStream<(Data, URLResponse), Swift.Error> } diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift b/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift index b5fc9b639..e79123e98 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift @@ -108,6 +108,7 @@ extension ImagePipeline { /// previews have ``ImageContainer/isPreview`` flag set to `true`. public var isStoringPreviewsInMemoryCache = true + // TODO: document that it was moved to `DataLoader` /// 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`, diff --git a/Sources/Nuke/Tasks/TaskFetchOriginalData.swift b/Sources/Nuke/Tasks/TaskFetchOriginalData.swift index 317e765a0..a2415028f 100644 --- a/Sources/Nuke/Tasks/TaskFetchOriginalData.swift +++ b/Sources/Nuke/Tasks/TaskFetchOriginalData.swift @@ -78,28 +78,41 @@ final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)> { self.resumableData = resumableData } - signpost(self, "LoadImageData", .begin, "URL: \(urlRequest.url?.absoluteString ?? ""), resumable data: \(Formatter.bytes(resumableData?.data.count ?? 0))") + signpost(self, "LoadImageData", .begin, "URL: \(String(describing: urlRequest.url))") 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 } - Task { - self.dataTask(didReceiveData: data, response: response) + + let task = Task { @ImagePipelineActor in + do { + for try await (data, response) in dataLoader.loadData(for: request) { + dataTask(didReceiveData: data, response: response) + } + dataTaskDidFinish(error: nil) + } catch { + dataTaskDidFinish(error: error) } - }, 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))") - Task { - self.dataTaskDidFinish(error: error) - } - }) + finish() // Finish the operation! + } + +// let dataTask = dataLoader.loadData(with: urlRequest, didReceiveData: { [weak self] data, response in +// guard let self else { return } +// Task { +// self.dataTask(didReceiveData: data, response: response) +// } +// }, completion: { [weak self] error in +// +//// signpost(self, "LoadImageData", .end, "Finished with size \(Formatter.bytes(self.data.count))") +//// Task { +//// self.dataTaskDidFinish(error: error) +//// } +// }) onCancelled = { [weak self] in guard let self else { return } signpost(self, "LoadImageData", .end, "Cancelled") - dataTask.cancel() + task.cancel() finish() // Finish the operation! self.tryToSaveResumableData() @@ -107,16 +120,18 @@ final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)> { } private func dataTask(didReceiveData chunk: Data, response: URLResponse) { + // TODO: (nuke13) reimplement resumable data in DataLoader + // 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 - } +// 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 { diff --git a/Tests/MockDataLoader.swift b/Tests/MockDataLoader.swift index 583898d6e..4abde6c23 100644 --- a/Tests/MockDataLoader.swift +++ b/Tests/MockDataLoader.swift @@ -7,14 +7,14 @@ import Nuke private let data: Data = Test.data(name: "fixture", extension: "jpeg") -private final class MockDataTask: Cancellable, @unchecked Sendable { +private final class MockDataTask: MockDataTaskProtocol, @unchecked Sendable { var _cancel: () -> Void = { } func cancel() { _cancel() } } -class MockDataLoader: DataLoading, @unchecked Sendable { +class MockDataLoader: MockDataLoading, DataLoading, @unchecked Sendable { static let DidStartTask = Notification.Name("com.github.kean.Nuke.Tests.MockDataLoader.DidStartTask") static let DidCancelTask = Notification.Name("com.github.kean.Nuke.Tests.MockDataLoader.DidCancelTask") @@ -26,7 +26,7 @@ class MockDataLoader: DataLoading, @unchecked Sendable { set { queue.isSuspended = newValue } } - func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> Cancellable { + func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> MockDataTaskProtocol { let task = MockDataTask() NotificationCenter.default.post(name: MockDataLoader.DidStartTask, object: self) @@ -61,3 +61,34 @@ class MockDataLoader: DataLoading, @unchecked Sendable { return task } } + +// Remove these and update to implement the actual protocol. +protocol MockDataLoading: DataLoading { + func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> MockDataTaskProtocol +} + +extension MockDataLoading where Self: DataLoading { + func loadData(for request: ImageRequest) -> AsyncThrowingStream<(Data, URLResponse), any Error> { + AsyncThrowingStream { continuation in + guard let urlRequest = request.urlRequest else { + return continuation.finish(throwing: URLError(.badURL)) + } + let task = loadData(with: urlRequest) { data, response in + continuation.yield((data, response)) + } completion: { error in + continuation.finish(throwing: error) + } + continuation.onTermination = { reason in + switch reason { + case .cancelled: task.cancel() + default: break + } + } + } + } +} + +protocol MockDataTaskProtocol { + func cancel() +} + diff --git a/Tests/MockProgressiveDataLoader.swift b/Tests/MockProgressiveDataLoader.swift index 84ff40dfe..76c8f4a69 100644 --- a/Tests/MockProgressiveDataLoader.swift +++ b/Tests/MockProgressiveDataLoader.swift @@ -7,12 +7,12 @@ import Nuke // One-shot data loader that servers data split into chunks, only send one chunk // per one `resume()` call. -final class MockProgressiveDataLoader: DataLoading, @unchecked Sendable { +final class MockProgressiveDataLoader: MockDataLoading, DataLoading, @unchecked Sendable { let urlResponse: HTTPURLResponse var chunks: [Data] let data = Test.data(name: "progressive", extension: "jpeg") - class _MockTask: Cancellable, @unchecked Sendable { + class _MockTask: MockDataTaskProtocol, @unchecked Sendable { func cancel() { // Do nothing } @@ -26,7 +26,7 @@ final class MockProgressiveDataLoader: DataLoading, @unchecked Sendable { self.chunks = Array(_createChunks(for: data, size: data.count / 3)) } - func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> Cancellable { + func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> MockDataTaskProtocol { self.didReceiveData = didReceiveData self.completion = completion self.resume() diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift index 1c7b575ba..7952616cb 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift @@ -279,7 +279,8 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable { // MARK: - ImageRequest with Async/Await - func testImageRequestWithAsyncAwaitSuccess() async throws { + // TODO: (nuke13) reimplemnet + func _testImageRequestWithAsyncAwaitSuccess() async throws { if #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) { // GIVEN let localURL = Test.url(forResource: "fixture", extension: "jpeg") @@ -297,7 +298,8 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable { } } - func testImageRequestWithAsyncAwaitFailure() async throws { + // TODO: (nuke13) reimplemnet + func _testImageRequestWithAsyncAwaitFailure() async throws { if #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) { // WHEN let request = ImageRequest(id: "test", data: { diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineResumableDataTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineResumableDataTests.swift index 8fc267d23..0c8fccf7e 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineResumableDataTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineResumableDataTests.swift @@ -48,13 +48,32 @@ class ImagePipelineResumableDataTests: XCTestCase { } } -private class _MockResumableDataLoader: DataLoading, @unchecked Sendable { +private class _MockResumableDataLoader: MockDataLoading, DataLoading, @unchecked Sendable { private let queue = DispatchQueue(label: "_MockResumableDataLoader") let data: Data = Test.data(name: "fixture", extension: "jpeg") let eTag: String = "img_01" - func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> Cancellable { + func loadData(for request: ImageRequest) -> AsyncThrowingStream<(Data, URLResponse), any Error> { + AsyncThrowingStream { continuation in + guard let urlRequest = request.urlRequest else { + return continuation.finish(throwing: URLError(.badURL)) + } + let task = loadData(with: urlRequest) { data, response in + continuation.yield((data, response)) + } completion: { error in + continuation.finish(throwing: error) + } + continuation.onTermination = { reason in + switch reason { + case .cancelled: task.cancel() + default: break + } + } + } + } + + func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> MockDataTaskProtocol { let headers = request.allHTTPHeaderFields let completion = UncheckedSendableBox(value: completion) @@ -118,7 +137,7 @@ private class _MockResumableDataLoader: DataLoading, @unchecked Sendable { return _Task() } - private class _Task: Cancellable, @unchecked Sendable { + private class _Task: MockDataTaskProtocol, @unchecked Sendable { func cancel() { } } } From f4a4b740ce56238669e9b4b24d1e7c211d512618 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 19:56:59 -0400 Subject: [PATCH 29/73] Update ResumableDataStore to use generic namespace ID --- Sources/Nuke/Internal/ResumableData.swift | 28 +++++++++---------- Sources/Nuke/Pipeline/ImagePipeline.swift | 4 +-- .../Nuke/Tasks/TaskFetchOriginalData.swift | 4 +-- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Sources/Nuke/Internal/ResumableData.swift b/Sources/Nuke/Internal/ResumableData.swift index 0a8edb0e7..513a484c2 100644 --- a/Sources/Nuke/Internal/ResumableData.swift +++ b/Sources/Nuke/Internal/ResumableData.swift @@ -67,29 +67,29 @@ final class ResumableDataStorage: @unchecked Sendable { static let shared = ResumableDataStorage() private let lock = NSLock() - private var registeredPipelines = Set() + private var namespaces = Set() private var cache: Cache? // MARK: Registration - func register(_ pipeline: ImagePipeline) { + func register(_ namespace: UUID) { lock.lock() defer { lock.unlock() } - if registeredPipelines.isEmpty { + if namespaces.isEmpty { // 32 MB cache = Cache(costLimit: 32000000, countLimit: 100) } - registeredPipelines.insert(pipeline.id) + namespaces.insert(namespace) } - func unregister(_ pipeline: ImagePipeline) { + func unregister(_ namespace: UUID) { lock.lock() defer { lock.unlock() } - registeredPipelines.remove(pipeline.id) - if registeredPipelines.isEmpty { + namespaces.remove(namespace) + if namespaces.isEmpty { cache = nil // Deallocate storage } } @@ -103,31 +103,31 @@ final class ResumableDataStorage: @unchecked Sendable { // MARK: Storage - func removeResumableData(for request: ImageRequest, pipeline: ImagePipeline) -> ResumableData? { + func removeResumableData(for request: ImageRequest, namespace: UUID) -> ResumableData? { lock.lock() defer { lock.unlock() } - guard let key = Key(request: request, pipeline: pipeline) else { return nil } + guard let key = Key(request: request, namespace: namespace) else { return nil } return cache?.removeValue(forKey: key) } - func storeResumableData(_ data: ResumableData, for request: ImageRequest, pipeline: ImagePipeline) { + func storeResumableData(_ data: ResumableData, for request: ImageRequest, namespace: UUID) { lock.lock() defer { lock.unlock() } - guard let key = Key(request: request, pipeline: pipeline) else { return } + guard let key = Key(request: request, namespace: namespace) else { return } cache?.set(data, forKey: key, cost: data.data.count) } private struct Key: Hashable { - let pipelineId: UUID + let namespace: UUID let imageId: String - init?(request: ImageRequest, pipeline: ImagePipeline) { + init?(request: ImageRequest, namespace: UUID) { guard let imageId = request.imageId else { return nil } - self.pipelineId = pipeline.id + self.namespace = namespace self.imageId = imageId } } diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 893aba0f0..29503f24d 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -48,7 +48,7 @@ public final class ImagePipeline { nonisolated(unsafe) var onTaskStarted: ((ImageTask) -> Void)? // Debug purposes deinit { - ResumableDataStorage.shared.unregister(self) + ResumableDataStorage.shared.unregister(id) } /// Initializes the instance with the given configuration. @@ -68,7 +68,7 @@ public final class ImagePipeline { self.tasksFetchOriginalImage = TaskPool(isCoalescingEnabled) self.tasksFetchOriginalData = TaskPool(isCoalescingEnabled) - ResumableDataStorage.shared.register(self) + ResumableDataStorage.shared.register(id) } /// A convenience way to initialize the pipeline with a closure. diff --git a/Sources/Nuke/Tasks/TaskFetchOriginalData.swift b/Sources/Nuke/Tasks/TaskFetchOriginalData.swift index a2415028f..5b69d09ab 100644 --- a/Sources/Nuke/Tasks/TaskFetchOriginalData.swift +++ b/Sources/Nuke/Tasks/TaskFetchOriginalData.swift @@ -70,7 +70,7 @@ final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)> { // 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) { + let resumableData = ResumableDataStorage.shared.removeResumableData(for: request, namespace: pipeline.id) { // 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 @@ -177,7 +177,7 @@ final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)> { if pipeline.configuration.isResumableDataEnabled, let response = urlResponse, !data.isEmpty, let resumableData = ResumableData(response: response, data: data) { - ResumableDataStorage.shared.storeResumableData(resumableData, for: request, pipeline: pipeline) + ResumableDataStorage.shared.storeResumableData(resumableData, for: request, namespace: pipeline.id) } } } From a43d235632bdc8c59fc39dd22a56f65d10f16a8b Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 19:59:13 -0400 Subject: [PATCH 30/73] Revert DataLoader to use URLRequest as parameter for now --- Sources/Nuke/Loading/DataLoader.swift | 7 ++-- Sources/Nuke/Loading/DataLoading.swift | 2 +- .../Nuke/Tasks/TaskFetchOriginalData.swift | 35 ++++++------------- Tests/MockDataLoader.swift | 7 ++-- 4 files changed, 15 insertions(+), 36 deletions(-) diff --git a/Sources/Nuke/Loading/DataLoader.swift b/Sources/Nuke/Loading/DataLoader.swift index 1c09fc8dc..6da747777 100644 --- a/Sources/Nuke/Loading/DataLoader.swift +++ b/Sources/Nuke/Loading/DataLoader.swift @@ -96,12 +96,9 @@ public final class DataLoader: DataLoading, @unchecked Sendable { #endif }() - public func loadData(for request: ImageRequest) -> AsyncThrowingStream<(Data, URLResponse), Swift.Error> { + public func loadData(for request: URLRequest) -> AsyncThrowingStream<(Data, URLResponse), Swift.Error> { AsyncThrowingStream { continuation in - guard let urlRequest = request.urlRequest else { - return continuation.finish(throwing: URLError(.badURL)) - } - let task = loadData(with: urlRequest) { data, response in + let task = loadData(with: request) { data, response in continuation.yield((data, response)) } completion: { error in continuation.finish(throwing: error) diff --git a/Sources/Nuke/Loading/DataLoading.swift b/Sources/Nuke/Loading/DataLoading.swift index c7700a47a..e5bef5279 100644 --- a/Sources/Nuke/Loading/DataLoading.swift +++ b/Sources/Nuke/Loading/DataLoading.swift @@ -10,5 +10,5 @@ public protocol DataLoading: Sendable { /// /// - returns: Sequence that can be called more than once if streaming /// is supported. - func loadData(for request: ImageRequest) -> AsyncThrowingStream<(Data, URLResponse), Swift.Error> + func loadData(for request: URLRequest) -> AsyncThrowingStream<(Data, URLResponse), Swift.Error> } diff --git a/Sources/Nuke/Tasks/TaskFetchOriginalData.swift b/Sources/Nuke/Tasks/TaskFetchOriginalData.swift index 5b69d09ab..75ebf0c85 100644 --- a/Sources/Nuke/Tasks/TaskFetchOriginalData.swift +++ b/Sources/Nuke/Tasks/TaskFetchOriginalData.swift @@ -84,7 +84,7 @@ final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)> { let task = Task { @ImagePipelineActor in do { - for try await (data, response) in dataLoader.loadData(for: request) { + for try await (data, response) in dataLoader.loadData(for: urlRequest) { dataTask(didReceiveData: data, response: response) } dataTaskDidFinish(error: nil) @@ -95,19 +95,6 @@ final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)> { finish() // Finish the operation! } -// let dataTask = dataLoader.loadData(with: urlRequest, didReceiveData: { [weak self] data, response in -// guard let self else { return } -// Task { -// self.dataTask(didReceiveData: data, response: response) -// } -// }, completion: { [weak self] error in -// -//// signpost(self, "LoadImageData", .end, "Finished with size \(Formatter.bytes(self.data.count))") -//// Task { -//// self.dataTaskDidFinish(error: error) -//// } -// }) - onCancelled = { [weak self] in guard let self else { return } @@ -120,18 +107,16 @@ final class TaskFetchOriginalData: AsyncPipelineTask<(Data, URLResponse?)> { } private func dataTask(didReceiveData chunk: Data, response: URLResponse) { - // TODO: (nuke13) reimplement resumable data in DataLoader - // 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 -// } + 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 { diff --git a/Tests/MockDataLoader.swift b/Tests/MockDataLoader.swift index 4abde6c23..367af57aa 100644 --- a/Tests/MockDataLoader.swift +++ b/Tests/MockDataLoader.swift @@ -68,12 +68,9 @@ protocol MockDataLoading: DataLoading { } extension MockDataLoading where Self: DataLoading { - func loadData(for request: ImageRequest) -> AsyncThrowingStream<(Data, URLResponse), any Error> { + func loadData(for request: URLRequest) -> AsyncThrowingStream<(Data, URLResponse), any Error> { AsyncThrowingStream { continuation in - guard let urlRequest = request.urlRequest else { - return continuation.finish(throwing: URLError(.badURL)) - } - let task = loadData(with: urlRequest) { data, response in + let task = loadData(with: request) { data, response in continuation.yield((data, response)) } completion: { error in continuation.finish(throwing: error) From 8befa467e14a5c3d55289e4ee1f49331c0975a72 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 20:05:09 -0400 Subject: [PATCH 31/73] Reimplement data loading with closure --- Sources/Nuke/Tasks/TaskFetchWithClosure.swift | 54 ++++++++----------- .../ImagePipelineAsyncAwaitTests.swift | 6 +-- 2 files changed, 23 insertions(+), 37 deletions(-) diff --git a/Sources/Nuke/Tasks/TaskFetchWithClosure.swift b/Sources/Nuke/Tasks/TaskFetchWithClosure.swift index d0d8dd7f1..3d34edb62 100644 --- a/Sources/Nuke/Tasks/TaskFetchWithClosure.swift +++ b/Sources/Nuke/Tasks/TaskFetchWithClosure.swift @@ -4,6 +4,8 @@ import Foundation +// TODO: (nuke13) stop inhereting from AsyncPipelineTask + /// Fetches data using the publisher provided with the request. /// Unlike `TaskFetchOriginalImageData`, there is no resumable data involved. final class TaskFetchWithClosure: AsyncPipelineTask<(Data, URLResponse?)> { @@ -26,6 +28,8 @@ final class TaskFetchWithClosure: AsyncPipelineTask<(Data, URLResponse?)> { } } + // TODO: (nuke13) implement in TaskFetchOriginalData using the same protocol + // This methods gets called inside data loading operation (Operation). private func loadData(finish: @escaping () -> Void) { guard !isDisposed else { @@ -37,38 +41,22 @@ final class TaskFetchWithClosure: AsyncPipelineTask<(Data, URLResponse?)> { return assertionFailure("This should never happen") } - // TODO: (nuke13) reimplement - -// let cancellable = closure.sink(receiveCompletion: { [weak self] result in -// finish() // Finish the operation! -// guard let self else { return } -// Task { @ImagePipelineActor in -// self.dataTaskDidFinish(result) -// } -// }, receiveValue: { [weak self] data in -// guard let self else { return } -// Task { @ImagePipelineActor in -// self.data.append(data) -// } -// }) -// -// onCancelled = { -// finish() -// cancellable.cancel() -// } + let task = Task { @ImagePipelineActor in + do { + let data = try await closure() + guard !data.isEmpty else { + throw ImagePipeline.Error.dataIsEmpty + } + storeDataInCacheIfNeeded(data) + send(value: (data, nil), isCompleted: true) + } catch { + send(error: .dataLoadingFailed(error: error)) + } + finish() // Finish the operation! + } + onCancelled = { + finish() + task.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/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift index 7952616cb..1c7b575ba 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift @@ -279,8 +279,7 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable { // MARK: - ImageRequest with Async/Await - // TODO: (nuke13) reimplemnet - func _testImageRequestWithAsyncAwaitSuccess() async throws { + func testImageRequestWithAsyncAwaitSuccess() async throws { if #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) { // GIVEN let localURL = Test.url(forResource: "fixture", extension: "jpeg") @@ -298,8 +297,7 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable { } } - // TODO: (nuke13) reimplemnet - func _testImageRequestWithAsyncAwaitFailure() async throws { + func testImageRequestWithAsyncAwaitFailure() async throws { if #available(iOS 15.0, tvOS 15.0, macOS 12.0, watchOS 8.0, *) { // WHEN let request = ImageRequest(id: "test", data: { From 2306a4e999ff3e179f4529326feb753d81c5c3b6 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 20:50:44 -0400 Subject: [PATCH 32/73] Update ImagePrefetcher to use Swift Concurrency (partially) --- Sources/Nuke/Pipeline/ImagePipeline.swift | 2 +- Sources/Nuke/Prefetching/ImagePrefetcher.swift | 9 ++++++--- Tests/NukeTests/ImagePrefetcherTests.swift | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 29503f24d..e0bd40729 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -272,7 +272,7 @@ public final class ImagePipeline { // MARK: - ImageTask (Internal) - private nonisolated func makeStartedImageTask(with request: ImageRequest, isDataTask: Bool = false, onEvent: ((ImageTask.Event, ImageTask) -> Void)? = nil) -> ImageTask { + nonisolated func makeStartedImageTask(with request: ImageRequest, isDataTask: Bool = false, onEvent: ((ImageTask.Event, ImageTask) -> Void)? = nil) -> ImageTask { let task = ImageTask(taskId: nextTaskId.incremented(), request: request, isDataTask: isDataTask, pipeline: self, onEvent: onEvent) delegate.imageTaskCreated(task, pipeline: self) return task diff --git a/Sources/Nuke/Prefetching/ImagePrefetcher.swift b/Sources/Nuke/Prefetching/ImagePrefetcher.swift index df2dd25b2..39c7a1ffd 100644 --- a/Sources/Nuke/Prefetching/ImagePrefetcher.swift +++ b/Sources/Nuke/Prefetching/ImagePrefetcher.swift @@ -19,7 +19,7 @@ public final class ImagePrefetcher { /// /// - 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 { + public var isPaused: Bool = false { didSet { queue.isSuspended = isPaused } } @@ -143,9 +143,12 @@ public final class ImagePrefetcher { return } - // TODO: use async/await + // TODO: (nuke13) verify that this works private func loadImage(task: PrefetchTask, finish: @escaping () -> Void) { - task.imageTask = pipeline._loadImage(with: task.request, isDataTask: destination == .diskCache, progress: nil) { [weak self] _ in + let imageTask = pipeline.makeStartedImageTask(with: task.request, isDataTask: destination == .diskCache) + task.imageTask = imageTask + Task { [weak self] in + _ = try? await imageTask.response self?._remove(task) finish() } diff --git a/Tests/NukeTests/ImagePrefetcherTests.swift b/Tests/NukeTests/ImagePrefetcherTests.swift index 57d058068..0b3f5b430 100644 --- a/Tests/NukeTests/ImagePrefetcherTests.swift +++ b/Tests/NukeTests/ImagePrefetcherTests.swift @@ -5,7 +5,7 @@ import XCTest @testable import Nuke -#warning("reimplement (remove from target") +// TODO: (nuke13) reimplement (needs to be added to the target) final class ImagePrefetcherTests: XCTestCase { private var pipeline: ImagePipeline! private var dataLoader: MockDataLoader! From 83dc0eb7a0c2a4c9990c2901a2892c660883d748 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 20:56:51 -0400 Subject: [PATCH 33/73] Update ImagePipeline callbacks to be main actor isolated --- Sources/Nuke/Pipeline/ImagePipeline.swift | 48 +++++--------- .../NukeExtensions/ImageViewExtensions.swift | 2 +- Sources/NukeUI/FetchImage.swift | 2 + Sources/NukeUI/LazyImageView.swift | 26 ++++---- .../ImagePipelineLoadDataTests.swift | 19 ------ .../ImagePipelineTests.swift | 21 +------ Tests/NukeUITests/FetchImageTests.swift | 62 ------------------- 7 files changed, 32 insertions(+), 148 deletions(-) diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index e0bd40729..6b9d052cc 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -148,7 +148,7 @@ public final class ImagePipeline { /// is finished. @discardableResult public nonisolated func loadImage( with url: URL, - completion: @escaping (_ result: Result) -> Void + completion: @MainActor @Sendable @escaping (_ result: Result) -> Void ) -> ImageTask { _loadImage(with: ImageRequest(url: url), progress: nil, completion: completion) } @@ -161,7 +161,7 @@ public final class ImagePipeline { /// is finished. @discardableResult public nonisolated func loadImage( with request: ImageRequest, - completion: @escaping (_ result: Result) -> Void + completion: @MainActor @Sendable @escaping (_ result: Result) -> Void ) -> ImageTask { _loadImage(with: request, progress: nil, completion: completion) } @@ -176,11 +176,10 @@ public final class ImagePipeline { /// is finished. @discardableResult public nonisolated func loadImage( with request: ImageRequest, - queue: DispatchQueue? = nil, - progress: ((_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)?, - completion: @escaping (_ result: Result) -> Void + progress: (@MainActor @Sendable (_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)?, + completion: @MainActor @Sendable @escaping (_ result: Result) -> Void ) -> ImageTask { - _loadImage(with: request, queue: queue, progress: { + _loadImage(with: request, progress: { progress?($0, $1.completed, $1.total) }, completion: completion) } @@ -188,12 +187,11 @@ public final class ImagePipeline { nonisolated func _loadImage( with request: ImageRequest, isDataTask: Bool = false, - queue callbackQueue: DispatchQueue? = nil, - progress: ((ImageResponse?, ImageTask.Progress) -> Void)?, - completion: @escaping (Result) -> Void + progress: (@MainActor @Sendable (ImageResponse?, ImageTask.Progress) -> Void)?, + completion: @MainActor @Sendable @escaping (Result) -> Void ) -> ImageTask { - makeStartedImageTask(with: request, isDataTask: isDataTask) { [weak self] event, task in - self?.dispatchCallback(to: callbackQueue) { + makeStartedImageTask(with: request, isDataTask: isDataTask) { event, task in + DispatchQueue.main.async { // The callback-based API guarantees that after cancellation no // event are called on the callback queue. guard !task.isCancelling else { return } @@ -207,29 +205,20 @@ public final class ImagePipeline { } } - // TODO: (nuke13): requires callbacks to be @MainActor @Sendable or deprecate this entire API - private nonisolated func dispatchCallback(to callbackQueue: DispatchQueue?, _ closure: @escaping () -> Void) { - let box = UncheckedSendableBox(value: closure) - (callbackQueue ?? .main).async { - box.value() - } - } - // 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 nonisolated func loadData(with request: ImageRequest, completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void) -> ImageTask { - _loadData(with: request, queue: nil, progress: nil, completion: completion) + @discardableResult public nonisolated func loadData(with request: ImageRequest, completion: @MainActor @Sendable @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void) -> ImageTask { + _loadData(with: request, progress: nil, completion: completion) } private nonisolated func _loadData( with request: ImageRequest, - queue: DispatchQueue?, - progress progressHandler: ((_ completed: Int64, _ total: Int64) -> Void)?, - completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void + progress progressHandler: (@MainActor @Sendable (_ completed: Int64, _ total: Int64) -> Void)?, + completion: @MainActor @Sendable @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void ) -> ImageTask { - _loadImage(with: request, isDataTask: true, queue: queue) { _, progress in + _loadImage(with: request, isDataTask: true) { _, progress in progressHandler?(progress.completed, progress.total) } completion: { result in let result = result.map { response in @@ -249,17 +238,14 @@ public final class ImagePipeline { /// /// - 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 nonisolated func loadData( with request: ImageRequest, - queue: DispatchQueue? = nil, - progress progressHandler: ((_ completed: Int64, _ total: Int64) -> Void)?, - completion: @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void + progress progressHandler: (@MainActor @Sendable (_ completed: Int64, _ total: Int64) -> Void)?, + completion: @MainActor @Sendable @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void ) -> ImageTask { - _loadImage(with: request, isDataTask: true, queue: queue) { _, progress in + _loadImage(with: request, isDataTask: true) { _, progress in progressHandler?(progress.completed, progress.total) } completion: { result in let result = result.map { response in diff --git a/Sources/NukeExtensions/ImageViewExtensions.swift b/Sources/NukeExtensions/ImageViewExtensions.swift index 82bd8d7ea..b1fca39ec 100644 --- a/Sources/NukeExtensions/ImageViewExtensions.swift +++ b/Sources/NukeExtensions/ImageViewExtensions.swift @@ -290,7 +290,7 @@ private final class ImageViewController { 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 + task = pipeline.loadImage(with: request, progress: { [weak self] response, completedCount, totalCount in if let response, options.isProgressiveRenderingEnabled { self?.handle(partialImage: response) } diff --git a/Sources/NukeUI/FetchImage.swift b/Sources/NukeUI/FetchImage.swift index e0fc9025c..f034f738f 100644 --- a/Sources/NukeUI/FetchImage.swift +++ b/Sources/NukeUI/FetchImage.swift @@ -194,6 +194,8 @@ public final class FetchImage: ObservableObject, Identifiable { // MARK: Load (Combine) + // TODO: (nuke13) deprecate these + /// Loads an image with the given publisher. /// /// - important: Some `FetchImage` features, such as progress reporting and diff --git a/Sources/NukeUI/LazyImageView.swift b/Sources/NukeUI/LazyImageView.swift index 5bc7fcff5..60252f363 100644 --- a/Sources/NukeUI/LazyImageView.swift +++ b/Sources/NukeUI/LazyImageView.swift @@ -290,22 +290,18 @@ public final class LazyImageView: _PlatformBaseView { 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) + let task = pipeline.loadImage(with: request, 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) diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift index b6cfb27a5..1adcc004e 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift @@ -91,25 +91,6 @@ class ImagePipelineLoadDataTests: XCTestCase { wait() } - // MARK: - Callback Queues - - func testChangingCallbackQueueLoadData() { - // GIVEN - let queue = DispatchQueue(label: "testChangingCallbackQueue") - let queueKey = DispatchSpecificKey() - queue.setSpecific(key: queueKey, value: ()) - - // WHEN/THEN - let expectation = self.expectation(description: "Image data Loaded") - pipeline.loadData(with: Test.request, queue: queue, progress: { _, _ in - XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey)) - }, completion: { _ in - XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey)) - expectation.fulfill() - }) - wait() - } - // MARK: - Errors func testLoadWithInvalidURL() throws { diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineTests.swift index 01e255b7e..b8cfe26f3 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineTests.swift @@ -81,26 +81,7 @@ class ImagePipelineTests: XCTestCase { wait() } - - // MARK: - Callback Queues - - func testChangingCallbackQueueLoadImage() { - // Given - let queue = DispatchQueue(label: "testChangingCallbackQueue") - let queueKey = DispatchSpecificKey() - queue.setSpecific(key: queueKey, value: ()) - - // When/Then - let expectation = self.expectation(description: "Image Loaded") - pipeline.loadImage(with: Test.request, queue: queue, progress: { _, _, _ in - XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey)) - }, completion: { _ in - XCTAssertNotNil(DispatchQueue.getSpecific(key: queueKey)) - expectation.fulfill() - }) - wait() - } - + // MARK: - Updating Priority func testDataLoadingPriorityUpdated() { diff --git a/Tests/NukeUITests/FetchImageTests.swift b/Tests/NukeUITests/FetchImageTests.swift index cab3c7bb6..ceb11ac46 100644 --- a/Tests/NukeUITests/FetchImageTests.swift +++ b/Tests/NukeUITests/FetchImageTests.swift @@ -106,66 +106,4 @@ class FetchImageTests: XCTestCase { image.priority = .high wait() } - - func testPublisherImageLoaded() throws { - // RECORD - let record = expect(image.$result.dropFirst()).toPublishSingleValue() - - // WHEN - image.load(pipeline.imagePublisher(with: Test.request)) - wait() - - // THEN - let result = try XCTUnwrap(try XCTUnwrap(record.last)) - XCTAssertTrue(result.isSuccess) - XCTAssertNotNil(image.image) - } - - func testPublisherIsLoadingUpdated() { - // RECORD - expect(image.$result.dropFirst()).toPublishSingleValue() - let isLoading = record(image.$isLoading) - - // WHEN - image.load(pipeline.imagePublisher(with: Test.request)) - wait() - - // THEN - XCTAssertEqual(isLoading.values, [false, true, false]) - } - - func testPublisherMemoryCacheLookup() throws { - // GIVEN - pipeline.cache[Test.request] = Test.container - - // WHEN - image.load(pipeline.imagePublisher(with: Test.request)) - - // THEN image loaded synchronously - let result = try XCTUnwrap(image.result) - XCTAssertTrue(result.isSuccess) - let response = try XCTUnwrap(result.value) - XCTAssertEqual(response.cacheType, .memory) - XCTAssertNotNil(image.image) - } - - func testRequestCancelledWhenTargetGetsDeallocated() { - dataLoader.isSuspended = true - - // Wrap everything in autorelease pool to make sure that imageView - // gets deallocated immediately. - autoreleasepool { - // Given an image view with an associated image task - expectNotification(ImagePipelineObserver.didCreateTask, object: observer) - image.load(pipeline.imagePublisher(with: Test.request)) - wait() - - // Expect the task to be cancelled automatically - expectNotification(ImagePipelineObserver.didCancelTask, object: observer) - - // When the fetch image instance is deallocated - image = nil - } - wait() - } } From b8d190dc1204fd7a40cb895537c4d72a48e390f5 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 20:57:48 -0400 Subject: [PATCH 34/73] Remove UncheckedSendableBox --- Sources/Nuke/ImageRequest.swift | 6 ------ .../ImagePipelineResumableDataTests.swift | 10 +++++----- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Sources/Nuke/ImageRequest.swift b/Sources/Nuke/ImageRequest.swift index cf67aa992..4a8fed30e 100644 --- a/Sources/Nuke/ImageRequest.swift +++ b/Sources/Nuke/ImageRequest.swift @@ -494,9 +494,3 @@ extension ImageRequest { } } } - -// TODO: (nuke13) remove -/// - warning: Avoid using it! -struct UncheckedSendableBox: @unchecked Sendable { - let value: Value -} diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineResumableDataTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineResumableDataTests.swift index 0c8fccf7e..8d002fa6f 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineResumableDataTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineResumableDataTests.swift @@ -76,8 +76,8 @@ private class _MockResumableDataLoader: MockDataLoading, DataLoading, @unchecked func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> MockDataTaskProtocol { let headers = request.allHTTPHeaderFields - let completion = UncheckedSendableBox(value: completion) - let didReceiveData = UncheckedSendableBox(value: didReceiveData) + let completion = completion + let didReceiveData = didReceiveData func sendChunks(_ chunks: [Data], of data: Data, statusCode: Int) { @Sendable func sendChunk(_ chunk: Data) { @@ -93,7 +93,7 @@ private class _MockResumableDataLoader: MockDataLoading, DataLoading, @unchecked ] )! - didReceiveData.value(chunk, response) + didReceiveData(chunk, response) } var chunks = chunks @@ -121,7 +121,7 @@ private class _MockResumableDataLoader: MockDataLoading, DataLoading, @unchecked sendChunks(chunks, of: remainingData, statusCode: 206) queue.async { - completion.value(nil) + completion(nil) } } else { // Send half of chunks. @@ -130,7 +130,7 @@ private class _MockResumableDataLoader: MockDataLoading, DataLoading, @unchecked sendChunks(chunks, of: data, statusCode: 200) queue.async { - completion.value(NSError(domain: NSURLErrorDomain, code: URLError.networkConnectionLost.rawValue, userInfo: [:])) + completion(NSError(domain: NSURLErrorDomain, code: URLError.networkConnectionLost.rawValue, userInfo: [:])) } } From d650baf7f55ac91675be7c1aa41cdf1434653a30 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 17 Aug 2024 21:02:41 -0400 Subject: [PATCH 35/73] Remove now redundant _loadData method --- Sources/Nuke/Pipeline/ImagePipeline.swift | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 6b9d052cc..6eca18bac 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -210,23 +210,7 @@ public final class ImagePipeline { /// Loads image data for the given request. The data doesn't get decoded /// or processed in any other way. @discardableResult public nonisolated func loadData(with request: ImageRequest, completion: @MainActor @Sendable @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void) -> ImageTask { - _loadData(with: request, progress: nil, completion: completion) - } - - private nonisolated func _loadData( - with request: ImageRequest, - progress progressHandler: (@MainActor @Sendable (_ completed: Int64, _ total: Int64) -> Void)?, - completion: @MainActor @Sendable @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void - ) -> ImageTask { - _loadImage(with: request, isDataTask: true) { _, progress in - progressHandler?(progress.completed, progress.total) - } completion: { result in - let result = result.map { response in - // Data should never be empty - (data: response.container.data ?? Data(), response: response.urlResponse) - } - completion(result) - } + loadData(with: request, progress: nil, completion: completion) } /// Loads the image data for the given request. The data doesn't get decoded From 70b92f03d6fcba0b81f250b40a711ed14ea47795 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 18 Aug 2024 13:08:57 -0400 Subject: [PATCH 36/73] Fix a concurrency warning in RateLimiter --- Sources/Nuke/Internal/RateLimiter.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Nuke/Internal/RateLimiter.swift b/Sources/Nuke/Internal/RateLimiter.swift index fa7b6f5f1..fa4a31475 100644 --- a/Sources/Nuke/Internal/RateLimiter.swift +++ b/Sources/Nuke/Internal/RateLimiter.swift @@ -18,7 +18,7 @@ final class RateLimiter { // 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 var bucket: TokenBucket private var pending = LinkedList() // fast append, fast remove first private var isExecutingPendingTasks = false @@ -55,7 +55,7 @@ final class RateLimiter { 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)) -#warning("correct?") +// TODO: make sure this is correct Task { try? await Task.sleep(nanoseconds: UInt64(bounds) * 1_000_000) self.executePendingTasks() @@ -73,7 +73,7 @@ final class RateLimiter { } } -private final class TokenBucket { +private struct TokenBucket { let rate: Double private let burst: Double // maximum bucket size private var bucket: Double @@ -89,7 +89,7 @@ private final class TokenBucket { } /// Returns `true` if the closure was executed, `false` if dropped. - func execute(_ work: () -> Bool) -> Bool { + mutating func execute(_ work: () -> Bool) -> Bool { refill() guard bucket >= 1.0 else { return false // bucket is empty @@ -100,7 +100,7 @@ private final class TokenBucket { return true } - private func refill() { + private mutating func refill() { let now = CFAbsoluteTimeGetCurrent() bucket += rate * max(0, now - timestamp) // rate * (time delta) timestamp = now From 123586a137eed6a2204b8360ab83dfe1f02f787a Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 18 Aug 2024 13:41:45 -0400 Subject: [PATCH 37/73] Fix RateLimiter tests --- Sources/Nuke/Internal/RateLimiter.swift | 1 - Sources/Nuke/Pipeline/ImagePipeline.swift | 2 +- Tests/NukeTests/RateLimiterTests.swift | 72 +++++++---------------- 3 files changed, 21 insertions(+), 54 deletions(-) diff --git a/Sources/Nuke/Internal/RateLimiter.swift b/Sources/Nuke/Internal/RateLimiter.swift index fa4a31475..85173fb3a 100644 --- a/Sources/Nuke/Internal/RateLimiter.swift +++ b/Sources/Nuke/Internal/RateLimiter.swift @@ -55,7 +55,6 @@ final class RateLimiter { 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)) -// TODO: make sure this is correct Task { try? await Task.sleep(nanoseconds: UInt64(bounds) * 1_000_000) self.executePendingTasks() diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 6eca18bac..8a268433f 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -184,7 +184,7 @@ public final class ImagePipeline { }, completion: completion) } - nonisolated func _loadImage( + private nonisolated func _loadImage( with request: ImageRequest, isDataTask: Bool = false, progress: (@MainActor @Sendable (ImageResponse?, ImageTask.Progress) -> Void)?, diff --git a/Tests/NukeTests/RateLimiterTests.swift b/Tests/NukeTests/RateLimiterTests.swift index a65ae37d1..c60afd52a 100644 --- a/Tests/NukeTests/RateLimiterTests.swift +++ b/Tests/NukeTests/RateLimiterTests.swift @@ -6,33 +6,25 @@ import XCTest @testable import Nuke class RateLimiterTests: XCTestCase { - var queue: DispatchQueue! - var queueKey: DispatchSpecificKey! var rateLimiter: RateLimiter! override func setUp() { super.setUp() - queue = DispatchQueue(label: "com.github.kean.rate-limiter-tests") - - queueKey = DispatchSpecificKey() - queue.setSpecific(key: queueKey, value: ()) - // Note: we set very short rate to avoid bucket form being refilled too quickly rateLimiter = RateLimiter(rate: 10, burst: 2) } + @ImagePipelineActor func testThatBurstIsExecutedimmediately() { // Given var isExecuted = Array(repeating: false, count: 4) // When for i in isExecuted.indices { - queue.sync { - rateLimiter.execute { - isExecuted[i] = true - return true - } + rateLimiter.execute { + isExecuted[i] = true + return true } } @@ -40,68 +32,44 @@ class RateLimiterTests: XCTestCase { XCTAssertEqual(isExecuted, [true, true, false, false], "Expect first 2 items to be executed immediately") } + @ImagePipelineActor func testThatNotExecutedItemDoesntExtractFromBucket() { // Given var isExecuted = Array(repeating: false, count: 4) // When for i in isExecuted.indices { - queue.sync { - rateLimiter.execute { - isExecuted[i] = true - return i != 1 // important! - } + rateLimiter.execute { + isExecuted[i] = true + return i != 1 // important! } } // Then XCTAssertEqual(isExecuted, [true, true, true, false], "Expect first 2 items to be executed immediately") } - + + @ImagePipelineActor func testOverflow() { // Given var isExecuted = Array(repeating: false, count: 3) - + // When let expectation = self.expectation(description: "All work executed") expectation.expectedFulfillmentCount = isExecuted.count - - queue.sync { - for i in isExecuted.indices { - rateLimiter.execute { - isExecuted[i] = true - expectation.fulfill() - return true - } + + for i in isExecuted.indices { + rateLimiter.execute { + isExecuted[i] = true + expectation.fulfill() + return true } } - + // When time is passed wait() - + // Then - queue.sync { - XCTAssertEqual(isExecuted, [true, true, true], "Expect 3rd item to be executed after a short delay") - } - } - - func testOverflowItemsExecutedOnSpecificQueue() { - // Given - let isExecuted = Array(repeating: false, count: 3) - - let expectation = self.expectation(description: "All work executed") - expectation.expectedFulfillmentCount = isExecuted.count - - queue.sync { - for _ in isExecuted.indices { - rateLimiter.execute { - expectation.fulfill() - // Then delayed task also executed on queue - XCTAssertNotNil(DispatchQueue.getSpecific(key: self.queueKey)) - return true - } - } - } - wait() + XCTAssertEqual(isExecuted, [true, true, true], "Expect 3rd item to be executed after a short delay") } } From 38dc11c550277add7c9e800453660feaf9901d49 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 18 Aug 2024 13:42:34 -0400 Subject: [PATCH 38/73] Remove one of the loadData variant --- Sources/Nuke/Pipeline/ImagePipeline.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 8a268433f..6114dc3b1 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -207,12 +207,6 @@ public final class ImagePipeline { // 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 nonisolated func loadData(with request: ImageRequest, completion: @MainActor @Sendable @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void) -> ImageTask { - loadData(with: request, progress: nil, completion: completion) - } - /// Loads the image data for the given request. The data doesn't get decoded /// or processed in any other way. /// @@ -226,7 +220,7 @@ public final class ImagePipeline { /// - completion: A closure to be called on the main thread when the request is finished. @discardableResult public nonisolated func loadData( with request: ImageRequest, - progress progressHandler: (@MainActor @Sendable (_ completed: Int64, _ total: Int64) -> Void)?, + progress progressHandler: (@MainActor @Sendable (_ completed: Int64, _ total: Int64) -> Void)? = nil, completion: @MainActor @Sendable @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void ) -> ImageTask { _loadImage(with: request, isDataTask: true) { _, progress in From 2e0e6b6c710198bffad929334c6d0a21a01fe2b5 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 18 Aug 2024 13:45:06 -0400 Subject: [PATCH 39/73] Rename makeImageTask --- Sources/Nuke/Pipeline/ImagePipeline.swift | 8 ++++---- Sources/Nuke/Prefetching/ImagePrefetcher.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 6114dc3b1..1cbf9afea 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -114,7 +114,7 @@ public final class ImagePipeline { /// /// The task starts executing the moment it is created. public nonisolated func imageTask(with request: ImageRequest) -> ImageTask { - makeStartedImageTask(with: request) + makeImageTask(with: request) } /// Returns an image for the given URL. @@ -133,7 +133,7 @@ public final class ImagePipeline { /// /// - parameter request: An image request. public func data(for request: ImageRequest) async throws -> (Data, URLResponse?) { - let task = makeStartedImageTask(with: request, isDataTask: true) + let task = makeImageTask(with: request, isDataTask: true) let response = try await task.response return (response.container.data ?? Data(), response.urlResponse) } @@ -190,7 +190,7 @@ public final class ImagePipeline { progress: (@MainActor @Sendable (ImageResponse?, ImageTask.Progress) -> Void)?, completion: @MainActor @Sendable @escaping (Result) -> Void ) -> ImageTask { - makeStartedImageTask(with: request, isDataTask: isDataTask) { event, task in + makeImageTask(with: request, isDataTask: isDataTask) { event, task in DispatchQueue.main.async { // The callback-based API guarantees that after cancellation no // event are called on the callback queue. @@ -236,7 +236,7 @@ public final class ImagePipeline { // MARK: - ImageTask (Internal) - nonisolated func makeStartedImageTask(with request: ImageRequest, isDataTask: Bool = false, onEvent: ((ImageTask.Event, ImageTask) -> Void)? = nil) -> ImageTask { + nonisolated func makeImageTask(with request: ImageRequest, isDataTask: Bool = false, onEvent: ((ImageTask.Event, ImageTask) -> Void)? = nil) -> ImageTask { let task = ImageTask(taskId: nextTaskId.incremented(), request: request, isDataTask: isDataTask, pipeline: self, onEvent: onEvent) delegate.imageTaskCreated(task, pipeline: self) return task diff --git a/Sources/Nuke/Prefetching/ImagePrefetcher.swift b/Sources/Nuke/Prefetching/ImagePrefetcher.swift index 39c7a1ffd..af852b042 100644 --- a/Sources/Nuke/Prefetching/ImagePrefetcher.swift +++ b/Sources/Nuke/Prefetching/ImagePrefetcher.swift @@ -145,7 +145,7 @@ public final class ImagePrefetcher { // TODO: (nuke13) verify that this works private func loadImage(task: PrefetchTask, finish: @escaping () -> Void) { - let imageTask = pipeline.makeStartedImageTask(with: task.request, isDataTask: destination == .diskCache) + let imageTask = pipeline.makeImageTask(with: task.request, isDataTask: destination == .diskCache) task.imageTask = imageTask Task { [weak self] in _ = try? await imageTask.response From 1b7547aff586697257f687049c9c4c29c2a7dec6 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 18 Aug 2024 13:46:16 -0400 Subject: [PATCH 40/73] Rename imageTaskUpdatePriorityCalled --- Sources/Nuke/ImageTask.swift | 2 +- Sources/Nuke/Pipeline/ImagePipeline.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index 4e7b8485d..54c075f43 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -196,7 +196,7 @@ public final class ImageTask: Hashable, @unchecked Sendable { return !$0.isCancelling }) else { return } Task { @ImagePipelineActor in - pipeline?.imageTaskUpdatePriorityCalled(self, priority: newValue) + pipeline?.imageTask(self, didChangePriority: newValue) } } diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 1cbf9afea..8bb528e96 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -259,7 +259,7 @@ public final class ImagePipeline { task._cancel() } - func imageTaskUpdatePriorityCalled(_ task: ImageTask, priority: ImageRequest.Priority) { + func imageTask(_ task: ImageTask, didChangePriority priority: ImageRequest.Priority) { self.tasks[task]?.setPriority(priority.taskPriority) } From 7e34b50c640e0a650ea16c81ac872d1583b09ff1 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 18 Aug 2024 13:49:36 -0400 Subject: [PATCH 41/73] Soft-deprecate closure-based APIs --- Nuke.xcodeproj/project.pbxproj | 6 +- .../Pipeline/ImagePipeline+Closures.swift | 107 ++++++++++++++++++ Sources/Nuke/Pipeline/ImagePipeline.swift | 100 +--------------- 3 files changed, 114 insertions(+), 99 deletions(-) create mode 100644 Sources/Nuke/Pipeline/ImagePipeline+Closures.swift diff --git a/Nuke.xcodeproj/project.pbxproj b/Nuke.xcodeproj/project.pbxproj index 6d813d42b..4585de9ca 100644 --- a/Nuke.xcodeproj/project.pbxproj +++ b/Nuke.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 0C1453A02657EFA7005E24B3 /* ImagePipelineObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C14539F2657EFA7005E24B3 /* ImagePipelineObserver.swift */; }; 0C1453A12657EFA7005E24B3 /* ImagePipelineObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C14539F2657EFA7005E24B3 /* ImagePipelineObserver.swift */; }; 0C16C85F2C7150C800B2A560 /* ImagePublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C88C578263DAF1E0061A008 /* ImagePublisherTests.swift */; }; + 0C16C8632C726B1B00B2A560 /* ImagePipeline+Closures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C16C8622C726B1B00B2A560 /* ImagePipeline+Closures.swift */; }; 0C179C7B2283597F008AB488 /* ImageEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C179C7A2283597F008AB488 /* ImageEncoding.swift */; }; 0C1B9880294E28D800C09310 /* Nuke.docc in Sources */ = {isa = PBXBuildFile; fileRef = 0C1B987F294E28D800C09310 /* Nuke.docc */; }; 0C1C201D29ABBF19004B38FD /* Nuke.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C9174901BAE99EE004A7905 /* Nuke.framework */; }; @@ -350,6 +351,7 @@ 0C0FD5D81CA47FE1002A78FB /* ImageProcessing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageProcessing.swift; sourceTree = ""; }; 0C0FD5D91CA47FE1002A78FB /* ImageRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageRequest.swift; sourceTree = ""; }; 0C14539F2657EFA7005E24B3 /* ImagePipelineObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePipelineObserver.swift; sourceTree = ""; }; + 0C16C8622C726B1B00B2A560 /* ImagePipeline+Closures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImagePipeline+Closures.swift"; sourceTree = ""; }; 0C179C772282AC50008AB488 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = ""; }; 0C179C7A2283597F008AB488 /* ImageEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageEncoding.swift; sourceTree = ""; }; 0C1B987F294E28D800C09310 /* Nuke.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Nuke.docc; sourceTree = ""; }; @@ -952,11 +954,12 @@ isa = PBXGroup; children = ( 0C0FD5D31CA47FE1002A78FB /* ImagePipeline.swift */, - 0CC04B092C5698D500F1164D /* ImagePipelineActor.swift */, 0CF1754B22913F9800A8946E /* ImagePipeline+Configuration.swift */, 0C53C8B0263C968200E62D03 /* ImagePipeline+Delegate.swift */, 0C78A2A6263F4E680051E0FF /* ImagePipeline+Cache.swift */, 0CBA07852852DA8B00CE29F4 /* ImagePipeline+Error.swift */, + 0C16C8622C726B1B00B2A560 /* ImagePipeline+Closures.swift */, + 0CC04B092C5698D500F1164D /* ImagePipelineActor.swift */, ); path = Pipeline; sourceTree = ""; @@ -1729,6 +1732,7 @@ 0CA4ECC426E685F500BAC8E5 /* ImageProcessors+GaussianBlur.swift in Sources */, 0CA4EC9B26E67D3000BAC8E5 /* ImageDecoders+Empty.swift in Sources */, 0CB26802208F2565004C83F4 /* DataCache.swift in Sources */, + 0C16C8632C726B1B00B2A560 /* ImagePipeline+Closures.swift in Sources */, 0CA4EC9F26E67D6200BAC8E5 /* ImageDecoderRegistry.swift in Sources */, 0CA4ECBA26E6850B00BAC8E5 /* Graphics.swift in Sources */, 0CA4ECB426E6844B00BAC8E5 /* ImageProcessors.swift in Sources */, diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Closures.swift b/Sources/Nuke/Pipeline/ImagePipeline+Closures.swift new file mode 100644 index 000000000..c041daf8e --- /dev/null +++ b/Sources/Nuke/Pipeline/ImagePipeline+Closures.swift @@ -0,0 +1,107 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). + +import Foundation + +extension ImagePipeline { + /// Loads an image for the given request. + /// + /// - warning: Soft-deprecated in Nuke 13.0. + /// + /// - parameters: + /// - request: An image request. + /// - completion: A closure to be called on the main thread when the request + /// is finished. + @discardableResult public nonisolated func loadImage( + with url: URL, + completion: @MainActor @Sendable @escaping (_ result: Result) -> Void + ) -> ImageTask { + _loadImage(with: ImageRequest(url: url), progress: nil, completion: completion) + } + + /// Loads an image for the given request. + /// + /// - warning: Soft-deprecated in Nuke 13.0. + /// + /// - parameters: + /// - request: An image request. + /// - completion: A closure to be called on the main thread when the request + /// is finished. + @discardableResult public nonisolated func loadImage( + with request: ImageRequest, + completion: @MainActor @Sendable @escaping (_ result: Result) -> Void + ) -> ImageTask { + _loadImage(with: request, progress: nil, completion: completion) + } + + /// Loads an image for the given request. + /// + /// - warning: Soft-deprecated in Nuke 13.0. + /// + /// - parameters: + /// - request: An image request. + /// - 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 nonisolated func loadImage( + with request: ImageRequest, + progress: (@MainActor @Sendable (_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)?, + completion: @MainActor @Sendable @escaping (_ result: Result) -> Void + ) -> ImageTask { + _loadImage(with: request, progress: { + progress?($0, $1.completed, $1.total) + }, 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. + /// + /// - warning: Soft-deprecated in Nuke 13.0. + /// + /// - parameters: + /// - request: An image request. + /// - 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 nonisolated func loadData( + with request: ImageRequest, + progress progressHandler: (@MainActor @Sendable (_ completed: Int64, _ total: Int64) -> Void)? = nil, + completion: @MainActor @Sendable @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void + ) -> ImageTask { + _loadImage(with: request, isDataTask: true) { _, progress in + progressHandler?(progress.completed, progress.total) + } completion: { result in + let result = result.map { response in + // Data should never be empty + (data: response.container.data ?? Data(), response: response.urlResponse) + } + completion(result) + } + } + + private nonisolated func _loadImage( + with request: ImageRequest, + isDataTask: Bool = false, + progress: (@MainActor @Sendable (ImageResponse?, ImageTask.Progress) -> Void)?, + completion: @MainActor @Sendable @escaping (Result) -> Void + ) -> ImageTask { + makeImageTask(with: request, isDataTask: isDataTask) { event, task in + DispatchQueue.main.async { + // The callback-based API guarantees that after cancellation no + // event are called on the callback queue. + guard !task.isCancelling else { return } + switch event { + case .progress(let value): progress?(nil, value) + case .preview(let response): progress?(response, task.currentProgress) + case .cancelled: break // The legacy APIs do not send cancellation events + case .finished(let result): completion(result) + } + } + } + } +} diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 8bb528e96..c738ffd04 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -101,7 +101,7 @@ public final class ImagePipeline { } } - // MARK: - Loading Images (Async/Await) + // MARK: - Loading Images /// Creates a task with the given URL. /// @@ -127,7 +127,7 @@ public final class ImagePipeline { try await imageTask(with: request).image } - // MARK: - Loading Data (Async/Await) + // MARK: - Loading Data /// Returns image data for the given request. /// @@ -138,102 +138,6 @@ public final class ImagePipeline { return (response.container.data ?? Data(), response.urlResponse) } - // 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 nonisolated func loadImage( - with url: URL, - completion: @MainActor @Sendable @escaping (_ result: Result) -> Void - ) -> ImageTask { - _loadImage(with: ImageRequest(url: url), 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 nonisolated func loadImage( - with request: ImageRequest, - completion: @MainActor @Sendable @escaping (_ result: Result) -> Void - ) -> ImageTask { - _loadImage(with: request, progress: nil, completion: completion) - } - - /// Loads an image for the given request. - /// - /// - parameters: - /// - request: An image request. - /// - 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 nonisolated func loadImage( - with request: ImageRequest, - progress: (@MainActor @Sendable (_ response: ImageResponse?, _ completed: Int64, _ total: Int64) -> Void)?, - completion: @MainActor @Sendable @escaping (_ result: Result) -> Void - ) -> ImageTask { - _loadImage(with: request, progress: { - progress?($0, $1.completed, $1.total) - }, completion: completion) - } - - private nonisolated func _loadImage( - with request: ImageRequest, - isDataTask: Bool = false, - progress: (@MainActor @Sendable (ImageResponse?, ImageTask.Progress) -> Void)?, - completion: @MainActor @Sendable @escaping (Result) -> Void - ) -> ImageTask { - makeImageTask(with: request, isDataTask: isDataTask) { event, task in - DispatchQueue.main.async { - // The callback-based API guarantees that after cancellation no - // event are called on the callback queue. - guard !task.isCancelling else { return } - switch event { - case .progress(let value): progress?(nil, value) - case .preview(let response): progress?(response, task.currentProgress) - case .cancelled: break // The legacy APIs do not send cancellation events - case .finished(let result): completion(result) - } - } - } - } - - // MARK: - Loading Data (Closures) - - /// 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. - /// - 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 nonisolated func loadData( - with request: ImageRequest, - progress progressHandler: (@MainActor @Sendable (_ completed: Int64, _ total: Int64) -> Void)? = nil, - completion: @MainActor @Sendable @escaping (Result<(data: Data, response: URLResponse?), Error>) -> Void - ) -> ImageTask { - _loadImage(with: request, isDataTask: true) { _, progress in - progressHandler?(progress.completed, progress.total) - } completion: { result in - let result = result.map { response in - // Data should never be empty - (data: response.container.data ?? Data(), response: response.urlResponse) - } - completion(result) - } - } - // MARK: - ImageTask (Internal) nonisolated func makeImageTask(with request: ImageRequest, isDataTask: Bool = false, onEvent: ((ImageTask.Event, ImageTask) -> Void)? = nil) -> ImageTask { From 97c3ccdbff6c24fb3dc732cf3ef1d1e7452a3e13 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 18 Aug 2024 13:56:52 -0400 Subject: [PATCH 42/73] Remove warning in isResumableDataEnabled --- Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift b/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift index e79123e98..b5fc9b639 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift @@ -108,7 +108,6 @@ extension ImagePipeline { /// previews have ``ImageContainer/isPreview`` flag set to `true`. public var isStoringPreviewsInMemoryCache = true - // TODO: document that it was moved to `DataLoader` /// 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`, From cd82f340b83842ba20986f58db33ea2dd9dd1ce5 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 18 Aug 2024 13:57:47 -0400 Subject: [PATCH 43/73] Remove Sendable conformance from AsyncPipelineTask --- Sources/Nuke/Tasks/AsyncPipelineTask.swift | 2 +- Sources/Nuke/Tasks/AsyncTask.swift | 1 - Sources/Nuke/Tasks/TaskFetchWithClosure.swift | 4 ---- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Sources/Nuke/Tasks/AsyncPipelineTask.swift b/Sources/Nuke/Tasks/AsyncPipelineTask.swift index 2d9cd5a08..1f5d97818 100644 --- a/Sources/Nuke/Tasks/AsyncPipelineTask.swift +++ b/Sources/Nuke/Tasks/AsyncPipelineTask.swift @@ -6,7 +6,7 @@ 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 AsyncPipelineTask: AsyncTask, @unchecked Sendable { +class AsyncPipelineTask: AsyncTask { let pipeline: ImagePipeline // A canonical request representing the unit work performed by the task. let request: ImageRequest diff --git a/Sources/Nuke/Tasks/AsyncTask.swift b/Sources/Nuke/Tasks/AsyncTask.swift index a1c561190..faa1da3e5 100644 --- a/Sources/Nuke/Tasks/AsyncTask.swift +++ b/Sources/Nuke/Tasks/AsyncTask.swift @@ -16,7 +16,6 @@ import Foundation /// @ImagePipelineActor class AsyncTask: AsyncTaskSubscriptionDelegate { - private struct Subscription { let closure: (Event) -> Void weak var subscriber: AnyObject? diff --git a/Sources/Nuke/Tasks/TaskFetchWithClosure.swift b/Sources/Nuke/Tasks/TaskFetchWithClosure.swift index 3d34edb62..7f7fa8996 100644 --- a/Sources/Nuke/Tasks/TaskFetchWithClosure.swift +++ b/Sources/Nuke/Tasks/TaskFetchWithClosure.swift @@ -4,8 +4,6 @@ import Foundation -// TODO: (nuke13) stop inhereting from AsyncPipelineTask - /// Fetches data using the publisher provided with the request. /// Unlike `TaskFetchOriginalImageData`, there is no resumable data involved. final class TaskFetchWithClosure: AsyncPipelineTask<(Data, URLResponse?)> { @@ -28,8 +26,6 @@ final class TaskFetchWithClosure: AsyncPipelineTask<(Data, URLResponse?)> { } } - // TODO: (nuke13) implement in TaskFetchOriginalData using the same protocol - // This methods gets called inside data loading operation (Operation). private func loadData(finish: @escaping () -> Void) { guard !isDisposed else { From 868da19d7678092f315defeeb28f13e3395537d2 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 18 Aug 2024 14:47:24 -0400 Subject: [PATCH 44/73] Remove unused isResumableDataEnabled --- Sources/Nuke/Loading/DataLoader.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Sources/Nuke/Loading/DataLoader.swift b/Sources/Nuke/Loading/DataLoader.swift index 6da747777..e1b3a2a0b 100644 --- a/Sources/Nuke/Loading/DataLoader.swift +++ b/Sources/Nuke/Loading/DataLoader.swift @@ -13,12 +13,6 @@ public final class DataLoader: DataLoading, @unchecked Sendable { /// default, `false`. public var prefersIncrementalDelivery = false - /// 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 - /// 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. From 84400f044845b976f5ed35b68228c324f5a4c402 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 18 Aug 2024 14:55:47 -0400 Subject: [PATCH 45/73] Remove redundant @unchecked from ImageProcessingOptions.Border --- Sources/Nuke/Processing/ImageProcessingOptions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Nuke/Processing/ImageProcessingOptions.swift b/Sources/Nuke/Processing/ImageProcessingOptions.swift index 2ace3a688..03d652503 100644 --- a/Sources/Nuke/Processing/ImageProcessingOptions.swift +++ b/Sources/Nuke/Processing/ImageProcessingOptions.swift @@ -34,7 +34,7 @@ public enum ImageProcessingOptions: Sendable { /// 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 struct Border: Hashable, CustomStringConvertible, Sendable { public let width: CGFloat #if canImport(UIKit) From 0e5273b1bf65955bf09ab54ad7a3ec4eefb42bfb Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 18 Aug 2024 14:58:38 -0400 Subject: [PATCH 46/73] Remove redundant @unchecked Sendable --- Sources/Nuke/Decoding/ImageDecoderRegistry.swift | 2 +- Sources/Nuke/Encoding/ImageEncoding.swift | 2 +- Sources/Nuke/ImageContainer.swift | 2 +- Sources/Nuke/ImageResponse.swift | 2 +- Sources/Nuke/Tasks/TaskLoadData.swift | 2 +- Sources/Nuke/Tasks/TaskLoadImage.swift | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/Nuke/Decoding/ImageDecoderRegistry.swift b/Sources/Nuke/Decoding/ImageDecoderRegistry.swift index 3c5b54650..8c86727d3 100644 --- a/Sources/Nuke/Decoding/ImageDecoderRegistry.swift +++ b/Sources/Nuke/Decoding/ImageDecoderRegistry.swift @@ -54,7 +54,7 @@ public final class ImageDecoderRegistry: @unchecked Sendable { } /// Image decoding context used when selecting which decoder to use. -public struct ImageDecodingContext: @unchecked Sendable { +public struct ImageDecodingContext: Sendable { public var request: ImageRequest public var data: Data /// Returns `true` if the download was completed. diff --git a/Sources/Nuke/Encoding/ImageEncoding.swift b/Sources/Nuke/Encoding/ImageEncoding.swift index 1385ea2e6..276245555 100644 --- a/Sources/Nuke/Encoding/ImageEncoding.swift +++ b/Sources/Nuke/Encoding/ImageEncoding.swift @@ -33,7 +33,7 @@ extension ImageEncoding { } /// Image encoding context used when selecting which encoder to use. -public struct ImageEncodingContext: @unchecked Sendable { +public struct ImageEncodingContext: Sendable { public let request: ImageRequest public let image: PlatformImage public let urlResponse: URLResponse? diff --git a/Sources/Nuke/ImageContainer.swift b/Sources/Nuke/ImageContainer.swift index 346a50622..086f4c410 100644 --- a/Sources/Nuke/ImageContainer.swift +++ b/Sources/Nuke/ImageContainer.swift @@ -19,7 +19,7 @@ public typealias PlatformImage = NSImage #endif /// An image container with an image and associated metadata. -public struct ImageContainer: @unchecked Sendable { +public struct ImageContainer: Sendable { #if os(macOS) /// A fetched image. public var image: NSImage { diff --git a/Sources/Nuke/ImageResponse.swift b/Sources/Nuke/ImageResponse.swift index 0999a8b68..dd5b3b0ea 100644 --- a/Sources/Nuke/ImageResponse.swift +++ b/Sources/Nuke/ImageResponse.swift @@ -13,7 +13,7 @@ import AppKit #endif /// An image response that contains a fetched image and some metadata. -public struct ImageResponse: @unchecked Sendable { +public struct ImageResponse: Sendable { /// An image container with an image and associated metadata. public var container: ImageContainer diff --git a/Sources/Nuke/Tasks/TaskLoadData.swift b/Sources/Nuke/Tasks/TaskLoadData.swift index c571c666e..02b75086d 100644 --- a/Sources/Nuke/Tasks/TaskLoadData.swift +++ b/Sources/Nuke/Tasks/TaskLoadData.swift @@ -5,7 +5,7 @@ import Foundation /// Wrapper for tasks created by `loadData` calls. -final class TaskLoadData: AsyncPipelineTask, @unchecked Sendable { +final class TaskLoadData: AsyncPipelineTask { override func start() { if let data = pipeline.cache.cachedData(for: request) { let container = ImageContainer(image: .init(), data: data) diff --git a/Sources/Nuke/Tasks/TaskLoadImage.swift b/Sources/Nuke/Tasks/TaskLoadImage.swift index 13a2a748a..260b3c9a6 100644 --- a/Sources/Nuke/Tasks/TaskLoadImage.swift +++ b/Sources/Nuke/Tasks/TaskLoadImage.swift @@ -9,7 +9,7 @@ import Foundation /// 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: AsyncPipelineTask, @unchecked Sendable { +final class TaskLoadImage: AsyncPipelineTask { override func start() { if let container = pipeline.cache[request] { let response = ImageResponse(container: container, request: request, cacheType: .memory) From 8ab81b7d002d82421c014a8ee0d6dd58b209c3c6 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 26 Oct 2024 16:37:31 -0400 Subject: [PATCH 47/73] Update ImagePrefetcher --- Nuke.xcodeproj/project.pbxproj | 2 + .../Nuke/Prefetching/ImagePrefetcher.swift | 51 +++++++++---------- .../ImagePipelineCoalescingTests.swift | 32 ++++++------ .../ImagePipelineDataCacheTests.swift | 6 +-- .../ImagePipelineLoadDataTests.swift | 8 +-- Tests/NukeTests/ImagePrefetcherTests.swift | 49 +++++------------- 6 files changed, 63 insertions(+), 85 deletions(-) diff --git a/Nuke.xcodeproj/project.pbxproj b/Nuke.xcodeproj/project.pbxproj index 4585de9ca..612ff5747 100644 --- a/Nuke.xcodeproj/project.pbxproj +++ b/Nuke.xcodeproj/project.pbxproj @@ -146,6 +146,7 @@ 0C8684FF20BDD578009FF7CC /* ImagePipelineProgressiveDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2A8CFA20970D8D0013FD65 /* ImagePipelineProgressiveDecodingTests.swift */; }; 0C86AB6A228B3B5100A81BA1 /* ImageTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C86AB69228B3B5100A81BA1 /* ImageTask.swift */; }; 0C880532242E7B1500F8C5B3 /* ImagePipelineDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C880531242E7B1500F8C5B3 /* ImagePipelineDecodingTests.swift */; }; + 0C8C614D2CCD760C00532008 /* ImagePrefetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD195291D4348AC00E011BB /* ImagePrefetcherTests.swift */; }; 0C8D7BD31D9DBF1600D12EB7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8D7BD21D9DBF1600D12EB7 /* AppDelegate.swift */; }; 0C8D7BD51D9DBF1600D12EB7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8D7BD41D9DBF1600D12EB7 /* ViewController.swift */; }; 0C8D7BD81D9DBF1600D12EB7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C8D7BD61D9DBF1600D12EB7 /* Main.storyboard */; }; @@ -1633,6 +1634,7 @@ 0C75279E1D473AEF00EC6222 /* MockImageCache.swift in Sources */, 0CCBB534217D0B980026F552 /* MockProgressiveDataLoader.swift in Sources */, 0C7CE28F24393ACC0018C8C3 /* CoreImageFilterTests.swift in Sources */, + 0C8C614D2CCD760C00532008 /* ImagePrefetcherTests.swift in Sources */, 0C53C8AF263C7B1700E62D03 /* ImagePipelineDelegateTests.swift in Sources */, 0C1453A02657EFA7005E24B3 /* ImagePipelineObserver.swift in Sources */, 0C1E620B1D6F817700AD5CF5 /* ImageRequestTests.swift in Sources */, diff --git a/Sources/Nuke/Prefetching/ImagePrefetcher.swift b/Sources/Nuke/Prefetching/ImagePrefetcher.swift index af852b042..9aeac7caa 100644 --- a/Sources/Nuke/Prefetching/ImagePrefetcher.swift +++ b/Sources/Nuke/Prefetching/ImagePrefetcher.swift @@ -13,25 +13,29 @@ import Foundation /// even from the main thread during scrolling. @ImagePipelineActor public final class ImagePrefetcher { - #warning("make these non-isolated") - - /// Pauses the prefetching. +/// 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 } + public nonisolated var isPaused: Bool { + get { queue.isSuspended } + set { queue.isSuspended = newValue } } /// 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 + public nonisolated var priority: ImageRequest.Priority { + get { _priority.value } + set { + guard _priority.withLock({ + guard $0 != newValue else { return false } + $0 = newValue + return true + }) else { return } Task { - self.didUpdatePriority(to: newValue) + await didUpdatePriority(to: newValue) } } } @@ -53,15 +57,15 @@ public final class ImagePrefetcher { /// 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: (@MainActor @Sendable () -> Void)? private let pipeline: ImagePipeline - private var tasks = [TaskLoadImageKey: PrefetchTask]() private let destination: Destination - private var _priority: ImageRequest.Priority = .low - let queue = OperationQueue() // internal for testing + private var tasks = [TaskLoadImageKey: PrefetchTask]() + private let _priority = Mutex(ImageRequest.Priority.low) + + // internal for testing + nonisolated let didComplete = Mutex<(@Sendable () -> Void)?>(nil) + nonisolated let queue = OperationQueue() /// Initializes the ``ImagePrefetcher`` instance. /// @@ -113,11 +117,12 @@ public final class ImagePrefetcher { } } - public func _startPrefetching(with requests: [ImageRequest]) { + private func _startPrefetching(with requests: [ImageRequest]) { + let priority = _priority.value for request in requests { var request = request - if _priority != request.priority { - request.priority = _priority + if priority != request.priority { + request.priority = priority } _startPrefetching(with: request) } @@ -143,7 +148,6 @@ public final class ImagePrefetcher { return } - // TODO: (nuke13) verify that this works private func loadImage(task: PrefetchTask, finish: @escaping () -> Void) { let imageTask = pipeline.makeImageTask(with: task.request, isDataTask: destination == .diskCache) task.imageTask = imageTask @@ -162,17 +166,14 @@ public final class ImagePrefetcher { } private func sendCompletionIfNeeded() { - guard tasks.isEmpty, let callback = didComplete else { - return - } - DispatchQueue.main.async(execute: callback) + if tasks.isEmpty { didComplete.value?() } } /// 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]) { + public nonisolated func stopPrefetching(with urls: [URL]) { stopPrefetching(with: urls.map { ImageRequest(url: $0) }) } @@ -207,8 +208,6 @@ public final class ImagePrefetcher { } private func didUpdatePriority(to priority: ImageRequest.Priority) { - guard _priority != priority else { return } - _priority = priority for task in tasks.values { task.imageTask?.priority = priority } diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineCoalescingTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineCoalescingTests.swift index b655b4100..62309dda0 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineCoalescingTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineCoalescingTests.swift @@ -31,7 +31,7 @@ class ImagePipelineCoalescingTests: XCTestCase { // When loading images for those requests // Then the correct proessors are applied. - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadImage(with: request1) { result in let image = result.value?.image XCTAssertEqual(image?.nk_test_processorIDs ?? [], ["1"]) @@ -58,7 +58,7 @@ class ImagePipelineCoalescingTests: XCTestCase { // When loading images for those requests // Then the correct proessors are applied. - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadImage(with: request1) { result in let image = result.value?.image XCTAssertEqual(image?.nk_test_processorIDs ?? [], ["1"]) @@ -87,7 +87,7 @@ class ImagePipelineCoalescingTests: XCTestCase { // When loading images for those requests // Then the correct proessors are applied. - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadImage(with: request1) { result in let image = result.value?.image XCTAssertEqual(image?.nk_test_processorIDs ?? [], ["1"]) @@ -111,7 +111,7 @@ class ImagePipelineCoalescingTests: XCTestCase { let request1 = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 0)) let request2 = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .returnCacheDataDontLoad, timeoutInterval: 0)) - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadImage(with: request1) expect(pipeline).toLoadImage(with: request2) } @@ -130,7 +130,7 @@ class ImagePipelineCoalescingTests: XCTestCase { let request2 = ImageRequest(url: Test.url, userInfo: [.scaleKey: 3]) // WHEN loading images for those requests - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadImage(with: request1) { result in // THEN guard let image = result.value?.image else { return XCTFail() } @@ -156,7 +156,7 @@ class ImagePipelineCoalescingTests: XCTestCase { let request1 = ImageRequest(url: Test.url, userInfo: [.thumbnailKey: ImageRequest.ThumbnailOptions(maxPixelSize: 400)]) let request2 = ImageRequest(url: Test.url) - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { // WHEN loading images for those requests expect(pipeline).toLoadImage(with: request1) { result in @@ -184,7 +184,7 @@ class ImagePipelineCoalescingTests: XCTestCase { let request1 = ImageRequest(url: Test.url) let request2 = ImageRequest(url: Test.url, userInfo: [.thumbnailKey: ImageRequest.ThumbnailOptions(maxPixelSize: 400)]) - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { // WHEN loading images for those requests expect(pipeline).toLoadImage(with: request1) { result in // THEN @@ -214,7 +214,7 @@ class ImagePipelineCoalescingTests: XCTestCase { let queueObserver = OperationQueueObserver(queue: pipeline.configuration.imageProcessingQueue) // When - suspendDataLoading(for: pipeline, expectedRequestCount: 3) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 3) { expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url, processors: [processors.make(id: "1")])) expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url, processors: [processors.make(id: "2")])) expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url, processors: [processors.make(id: "1")])) @@ -428,7 +428,7 @@ class ImagePipelineCoalescingTests: XCTestCase { // MARK: - Loading Data func testThatLoadsDataOnceWhenLoadingDataAndLoadingImage() { - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadImage(with: Test.request) expect(pipeline).toLoadData(with: Test.request) } @@ -446,7 +446,7 @@ class ImagePipelineCoalescingTests: XCTestCase { ) // When/Then - suspendDataLoading(for: pipeline, expectedRequestCount: 3) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 3) { for _ in 0..<3 { let request = Test.request @@ -475,7 +475,7 @@ class ImagePipelineCoalescingTests: XCTestCase { } // When/Then - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadImage(with: Test.request) expect(pipeline).toLoadImage(with: Test.request) } @@ -507,7 +507,7 @@ class ImagePipelineProcessingDeduplicationTests: XCTestCase { let request2 = ImageRequest(url: Test.url, processors: [processors.make(id: "1"), processors.make(id: "2")]) // When - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadImage(with: request1) { result in let image = result.value?.image XCTAssertEqual(image?.nk_test_processorIDs ?? [], ["1"]) @@ -536,7 +536,7 @@ class ImagePipelineProcessingDeduplicationTests: XCTestCase { let request2 = ImageRequest(url: Test.url, processors: [processors.make(id: "1"), processors.make(id: "2"), processors.make(id: "3")]) // When - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadImage(with: request1) expect(pipeline).toLoadImage(with: request2) } @@ -613,7 +613,7 @@ class ImagePipelineProcessingDeduplicationTests: XCTestCase { let request2 = ImageRequest(url: Test.url, processors: [processors.make(id: "1"), processors.make(id: "2")]) // When - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadImage(with: request1) { result in let image = result.value?.image XCTAssertEqual(image?.nk_test_processorIDs ?? [], ["1"]) @@ -642,7 +642,7 @@ class ImagePipelineProcessingDeduplicationTests: XCTestCase { func makeRequest(options: ImageRequest.Options) -> ImageRequest { ImageRequest(urlRequest: URLRequest(url: Test.url), options: options) } - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadImage(with: makeRequest(options: [])) expect(pipeline).toLoadImage(with: makeRequest(options: [.reloadIgnoringCachedData])) } @@ -666,7 +666,7 @@ class ImagePipelineProcessingDeduplicationTests: XCTestCase { ImageRequest(urlRequest: URLRequest(url: Test.url), options: options) } - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadImage(with: makeRequest(options: [])) expect(pipeline).toLoadImage(with: makeRequest(options: [.reloadIgnoringCachedData])) } diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineDataCacheTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineDataCacheTests.swift index 0e07d435c..4b2bb3b12 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineDataCacheTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineDataCacheTests.swift @@ -393,7 +393,7 @@ class ImagePipelineDataCachePolicyTests: XCTestCase { } // WHEN - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "p1")])) expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url)) } @@ -480,7 +480,7 @@ class ImagePipelineDataCachePolicyTests: XCTestCase { } // WHEN - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "p1")])) expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url)) } @@ -545,7 +545,7 @@ class ImagePipelineDataCachePolicyTests: XCTestCase { } // WHEN - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "p1")])) expect(pipeline).toLoadImage(with: ImageRequest(url: Test.url)) } diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift index 1adcc004e..9516c37f8 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineLoadDataTests.swift @@ -196,7 +196,7 @@ extension ImagePipelineLoadDataTests { } // WHEN - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadData(with: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "p1")])) expect(pipeline).toLoadData(with: ImageRequest(url: Test.url)) } @@ -260,7 +260,7 @@ extension ImagePipelineLoadDataTests { } // WHEN - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadData(with: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "p1")])) expect(pipeline).toLoadData(with: ImageRequest(url: Test.url)) } @@ -386,7 +386,7 @@ extension ImagePipelineLoadDataTests { } // WHEN - suspendDataLoading(for: pipeline, expectedRequestCount: 2) { + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { expect(pipeline).toLoadData(with: ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "p1")])) expect(pipeline).toLoadData(with: ImageRequest(url: Test.url)) } @@ -403,7 +403,7 @@ extension ImagePipelineLoadDataTests { } extension XCTestCase { - func suspendDataLoading(for pipeline: ImagePipeline, expectedRequestCount count: Int, _ closure: () -> Void) { + func withSuspendedDataLoader(for pipeline: ImagePipeline, expectedRequestCount count: Int, _ closure: () -> Void) { let dataLoader = pipeline.configuration.dataLoader as! MockDataLoader dataLoader.isSuspended = true let expectation = self.expectation(description: "registered") diff --git a/Tests/NukeTests/ImagePrefetcherTests.swift b/Tests/NukeTests/ImagePrefetcherTests.swift index 0b3f5b430..3332940cc 100644 --- a/Tests/NukeTests/ImagePrefetcherTests.swift +++ b/Tests/NukeTests/ImagePrefetcherTests.swift @@ -5,7 +5,6 @@ import XCTest @testable import Nuke -// TODO: (nuke13) reimplement (needs to be added to the target) final class ImagePrefetcherTests: XCTestCase { private var pipeline: ImagePipeline! private var dataLoader: MockDataLoader! @@ -39,15 +38,10 @@ final class ImagePrefetcherTests: XCTestCase { /// Start prefetching for the request and then request an image separarely. func testBasicScenario() { - dataLoader.isSuspended = true - - expect(prefetcher.queue).toEnqueueOperationsWithCount(1) - prefetcher.startPrefetching(with: [Test.request]) - wait() - - expect(pipeline).toLoadImage(with: Test.request) - pipeline.queue.async { [dataLoader] in - dataLoader?.isSuspended = false + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 2) { + expect(prefetcher.queue).toEnqueueOperationsWithCount(1) + prefetcher.startPrefetching(with: [Test.request]) + expect(pipeline).toLoadImage(with: Test.request) } wait() @@ -72,15 +66,12 @@ final class ImagePrefetcherTests: XCTestCase { } func testStartPrefetchingWithTwoEquivalentURLs() { - dataLoader.isSuspended = true - expectPrefetcherToComplete() - - // WHEN - prefetcher.startPrefetching(with: [Test.url]) - prefetcher.startPrefetching(with: [Test.url]) + withSuspendedDataLoader(for: pipeline, expectedRequestCount: 1) { + expectPrefetcherToComplete() - pipeline.queue.async { [dataLoader] in - dataLoader?.isSuspended = false + // WHEN + prefetcher.startPrefetching(with: [Test.url]) + prefetcher.startPrefetching(with: [Test.url]) } wait() @@ -88,20 +79,6 @@ final class ImagePrefetcherTests: XCTestCase { XCTAssertEqual(observer.createdTaskCount, 1) } - func testWhenImageIsInMemoryCacheNoTaskStarted() { - dataLoader.isSuspended = true - - // GIVEN - pipeline.cache[Test.request] = Test.container - - // WHEN - prefetcher.startPrefetching(with: [Test.url]) - pipeline.queue.sync {} - - // THEN - XCTAssertEqual(observer.createdTaskCount, 0) - } - // MARK: Stop Prefetching func testStopPrefetching() { @@ -150,7 +127,7 @@ final class ImagePrefetcherTests: XCTestCase { prefetcher.startPrefetching(with: [Test.url]) let expectation = self.expectation(description: "TimePassed") - pipeline.queue.asyncAfter(deadline: .now() + .milliseconds(10)) { + DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(20)) { expectation.fulfill() } wait() @@ -247,7 +224,7 @@ final class ImagePrefetcherTests: XCTestCase { func testDidCompleteIsCalled() { let expectation = self.expectation(description: "PrefecherDidComplete") - prefetcher.didComplete = { @MainActor @Sendable in + prefetcher.didComplete.value = { @Sendable in expectation.fulfill() } @@ -257,7 +234,7 @@ final class ImagePrefetcherTests: XCTestCase { func testDidCompleteIsCalledWhenImageCached() { let expectation = self.expectation(description: "PrefecherDidComplete") - prefetcher.didComplete = { @MainActor @Sendable in + prefetcher.didComplete.value = { @Sendable in expectation.fulfill() } @@ -286,7 +263,7 @@ final class ImagePrefetcherTests: XCTestCase { func expectPrefetcherToComplete() { let expectation = self.expectation(description: "PrefecherDidComplete") - prefetcher.didComplete = { @MainActor @Sendable in + prefetcher.didComplete.value = { @Sendable in expectation.fulfill() } } From 1c495bd9d8957590af17b979861f0021ab86f394 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 26 Oct 2024 16:51:20 -0400 Subject: [PATCH 48/73] Update TaskTests --- Nuke.xcodeproj/project.pbxproj | 2 ++ Sources/Nuke/Tasks/AsyncTask.swift | 2 ++ Sources/Nuke/Tasks/TaskFetchOriginalImage.swift | 2 -- Sources/Nuke/Tasks/TaskLoadImage.swift | 1 - Tests/NukeTests/TaskTests.swift | 3 +-- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Nuke.xcodeproj/project.pbxproj b/Nuke.xcodeproj/project.pbxproj index 612ff5747..6a2d923d1 100644 --- a/Nuke.xcodeproj/project.pbxproj +++ b/Nuke.xcodeproj/project.pbxproj @@ -147,6 +147,7 @@ 0C86AB6A228B3B5100A81BA1 /* ImageTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C86AB69228B3B5100A81BA1 /* ImageTask.swift */; }; 0C880532242E7B1500F8C5B3 /* ImagePipelineDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C880531242E7B1500F8C5B3 /* ImagePipelineDecodingTests.swift */; }; 0C8C614D2CCD760C00532008 /* ImagePrefetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD195291D4348AC00E011BB /* ImagePrefetcherTests.swift */; }; + 0C8C614E2CCD8D4500532008 /* TaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0F7BF02287F6EE0034E656 /* TaskTests.swift */; }; 0C8D7BD31D9DBF1600D12EB7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8D7BD21D9DBF1600D12EB7 /* AppDelegate.swift */; }; 0C8D7BD51D9DBF1600D12EB7 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8D7BD41D9DBF1600D12EB7 /* ViewController.swift */; }; 0C8D7BD81D9DBF1600D12EB7 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C8D7BD61D9DBF1600D12EB7 /* Main.storyboard */; }; @@ -1651,6 +1652,7 @@ 0C91B0F42438E38B007F9100 /* CompositionTests.swift in Sources */, 0C91B0F62438E3CB007F9100 /* GaussianBlurTests.swift in Sources */, 0C6D0A8820E574400037B68F /* MockDataCache.swift in Sources */, + 0C8C614E2CCD8D4500532008 /* TaskTests.swift in Sources */, 0C472F812654AA46007FC0F0 /* DeprecationTests.swift in Sources */, 0C9B6E7620B9F3E2001924B8 /* ImagePipelineCoalescingTests.swift in Sources */, 0C91B0F22438E374007F9100 /* AnonymousTests.swift in Sources */, diff --git a/Sources/Nuke/Tasks/AsyncTask.swift b/Sources/Nuke/Tasks/AsyncTask.swift index faa1da3e5..c5d06a5d7 100644 --- a/Sources/Nuke/Tasks/AsyncTask.swift +++ b/Sources/Nuke/Tasks/AsyncTask.swift @@ -75,6 +75,8 @@ class AsyncTask: AsyncTaskSubscriptionDelegate /// Override this to start image task. Only gets called once. func start() {} + init() {} + // MARK: - Managing Observers /// - notes: Returns `nil` if the task was disposed. diff --git a/Sources/Nuke/Tasks/TaskFetchOriginalImage.swift b/Sources/Nuke/Tasks/TaskFetchOriginalImage.swift index c353210bb..9fd668fa2 100644 --- a/Sources/Nuke/Tasks/TaskFetchOriginalImage.swift +++ b/Sources/Nuke/Tasks/TaskFetchOriginalImage.swift @@ -37,8 +37,6 @@ final class TaskFetchOriginalImage: AsyncPipelineTask { } return } - - #warning("implement using async/await") decode(context, decoder: decoder) { [weak self] result in guard let self else { return } Task { diff --git a/Sources/Nuke/Tasks/TaskLoadImage.swift b/Sources/Nuke/Tasks/TaskLoadImage.swift index 260b3c9a6..e15b49c8a 100644 --- a/Sources/Nuke/Tasks/TaskLoadImage.swift +++ b/Sources/Nuke/Tasks/TaskLoadImage.swift @@ -30,7 +30,6 @@ final class TaskLoadImage: AsyncPipelineTask { guard let decoder = pipeline.delegate.imageDecoder(for: context, pipeline: pipeline) else { return didFinishDecoding(with: nil) } - #warning("implement using async/awiat") decode(context, decoder: decoder) { [weak self] result in guard let self else { return } Task { diff --git a/Tests/NukeTests/TaskTests.swift b/Tests/NukeTests/TaskTests.swift index 20d7f339b..51e962fa6 100644 --- a/Tests/NukeTests/TaskTests.swift +++ b/Tests/NukeTests/TaskTests.swift @@ -5,8 +5,7 @@ import XCTest @testable import Nuke -#warning("reimplement") - +@ImagePipelineActor class TaskTests: XCTestCase { // MARK: - Starter From 6df73d21fb2c7bc16ea1bba18c94d9ca69c2f28e Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 26 Oct 2024 16:54:55 -0400 Subject: [PATCH 49/73] Update testCancelAsyncImageTask --- .../ImagePipelineAsyncAwaitTests.swift | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift index 1c7b575ba..04df67649 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineAsyncAwaitTests.swift @@ -152,25 +152,22 @@ class ImagePipelineAsyncAwaitTests: XCTestCase, @unchecked Sendable { XCTAssertEqual(recordedProgress, []) } - #warning("reimplement") -// func testCancelAsyncImageTask() async throws { -// dataLoader.queue.isSuspended = true -// -// pipeline.queue.suspend() -// let task = pipeline.imageTask(with: Test.url) -// observer = NotificationCenter.default.addObserver(forName: MockDataLoader.DidStartTask, object: dataLoader, queue: OperationQueue()) { _ in -// task.cancel() -// } -// pipeline.queue.resume() -// -// var caughtError: Error? -// do { -// _ = try await task.image -// } catch { -// caughtError = error -// } -// XCTAssertTrue(caughtError is CancellationError) -// } + func testCancelAsyncImageTask() async throws { + dataLoader.queue.isSuspended = true + + let task = pipeline.imageTask(with: Test.url) + observer = NotificationCenter.default.addObserver(forName: MockDataLoader.DidStartTask, object: dataLoader, queue: OperationQueue()) { _ in + task.cancel() + } + + var caughtError: Error? + do { + _ = try await task.image + } catch { + caughtError = error + } + XCTAssertTrue(caughtError is CancellationError) + } // MARK: - Load Data From 6dadf6eeefb48de83ab4110ca93481421765ab6f Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 26 Oct 2024 16:56:24 -0400 Subject: [PATCH 50/73] Simplify didComplete --- Sources/Nuke/Pipeline/ImagePipeline.swift | 5 +++-- Sources/Nuke/Prefetching/ImagePrefetcher.swift | 6 +++--- Tests/NukeTests/ImagePrefetcherTests.swift | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index c738ffd04..3645d75c6 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -44,8 +44,9 @@ public final class ImagePipeline { let rateLimiter: RateLimiter? let id = UUID() -// TODO: remove - nonisolated(unsafe) var onTaskStarted: ((ImageTask) -> Void)? // Debug purposes + + // For testing purposes + nonisolated(unsafe) var onTaskStarted: ((ImageTask) -> Void)? deinit { ResumableDataStorage.shared.unregister(id) diff --git a/Sources/Nuke/Prefetching/ImagePrefetcher.swift b/Sources/Nuke/Prefetching/ImagePrefetcher.swift index 9aeac7caa..f95ea6880 100644 --- a/Sources/Nuke/Prefetching/ImagePrefetcher.swift +++ b/Sources/Nuke/Prefetching/ImagePrefetcher.swift @@ -63,8 +63,8 @@ public final class ImagePrefetcher { private var tasks = [TaskLoadImageKey: PrefetchTask]() private let _priority = Mutex(ImageRequest.Priority.low) - // internal for testing - nonisolated let didComplete = Mutex<(@Sendable () -> Void)?>(nil) + // For testing purposes + nonisolated(unsafe) var didComplete: (@Sendable () -> Void)? nonisolated let queue = OperationQueue() /// Initializes the ``ImagePrefetcher`` instance. @@ -166,7 +166,7 @@ public final class ImagePrefetcher { } private func sendCompletionIfNeeded() { - if tasks.isEmpty { didComplete.value?() } + if tasks.isEmpty { didComplete?() } } /// Stops prefetching images for the given URLs and cancels outstanding diff --git a/Tests/NukeTests/ImagePrefetcherTests.swift b/Tests/NukeTests/ImagePrefetcherTests.swift index 3332940cc..b98664565 100644 --- a/Tests/NukeTests/ImagePrefetcherTests.swift +++ b/Tests/NukeTests/ImagePrefetcherTests.swift @@ -224,7 +224,7 @@ final class ImagePrefetcherTests: XCTestCase { func testDidCompleteIsCalled() { let expectation = self.expectation(description: "PrefecherDidComplete") - prefetcher.didComplete.value = { @Sendable in + prefetcher.didComplete = { @Sendable in expectation.fulfill() } @@ -234,7 +234,7 @@ final class ImagePrefetcherTests: XCTestCase { func testDidCompleteIsCalledWhenImageCached() { let expectation = self.expectation(description: "PrefecherDidComplete") - prefetcher.didComplete.value = { @Sendable in + prefetcher.didComplete = { @Sendable in expectation.fulfill() } @@ -263,7 +263,7 @@ final class ImagePrefetcherTests: XCTestCase { func expectPrefetcherToComplete() { let expectation = self.expectation(description: "PrefecherDidComplete") - prefetcher.didComplete.value = { @Sendable in + prefetcher.didComplete = { @Sendable in expectation.fulfill() } } From 3fbd5b0270dbcc615ab4fc4c71e9adf0bf6fda53 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 26 Oct 2024 16:57:39 -0400 Subject: [PATCH 51/73] Deprecate FetchImage.load with Publisher --- Sources/NukeUI/FetchImage.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Sources/NukeUI/FetchImage.swift b/Sources/NukeUI/FetchImage.swift index f034f738f..eef3c12d3 100644 --- a/Sources/NukeUI/FetchImage.swift +++ b/Sources/NukeUI/FetchImage.swift @@ -194,13 +194,8 @@ public final class FetchImage: ObservableObject, Identifiable { // MARK: Load (Combine) - // TODO: (nuke13) deprecate these - - /// 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. + // Deprecated in Nuke 13.0 + @available(*, deprecated, message: "Please use Async/Await instead") public func load(_ publisher: P) where P.Output == ImageResponse { reset() From 2086dc62f8afb3453e0755cde38264c4d7ccb3f8 Mon Sep 17 00:00:00 2001 From: kean Date: Sat, 26 Oct 2024 16:58:42 -0400 Subject: [PATCH 52/73] Update tests --- .../ImagePipelineTests/ImagePipelineCoalescingTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineCoalescingTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineCoalescingTests.swift index 62309dda0..65adfc6b1 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineCoalescingTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineCoalescingTests.swift @@ -630,8 +630,7 @@ class ImagePipelineProcessingDeduplicationTests: XCTestCase { } } - // TODO: pipeline.queue.sync {} is no longer enough - func _testThatDataOnlyLoadedOnceWithDifferentCachePolicy() { + func testThatDataOnlyLoadedOnceWithDifferentCachePolicy() { // Given let dataCache = MockDataCache() pipeline = pipeline.reconfigured { From bf797252278034de36bfad553b334120af8801e7 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 27 Oct 2024 07:21:14 -0400 Subject: [PATCH 53/73] Fix an issue with state being writable --- Sources/Nuke/ImageTask.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index 54c075f43..7e6609add 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -13,6 +13,9 @@ import UIKit import AppKit #endif +// TODO: try to make another internal instance that is isolated +// - possible make `ImageTask` a struct? then no questions about retaining it + /// A task performed by the ``ImagePipeline``. /// /// The pipeline maintains a strong reference to the task until the request @@ -60,7 +63,7 @@ public final class ImageTask: Hashable, @unchecked Sendable { /// The current state of the task. @ImagePipelineActor - public var state: State = .running + public private(set) var state: State = .running /// The state of the image task. public enum State: Sendable { From bca4607b1e3a860b485a6d1a1a91f07e9cc4546d Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 27 Oct 2024 10:00:32 -0400 Subject: [PATCH 54/73] Sync ImageTask on ImagePipelineActor --- Sources/Nuke/ImageTask.swift | 53 ++++++++----------- Sources/Nuke/Pipeline/ImagePipeline.swift | 4 +- Tests/MockDataLoader.swift | 6 +-- Tests/MockProgressiveDataLoader.swift | 2 +- .../ImagePipelineResumableDataTests.swift | 2 +- Tests/XCTestCase+Nuke.swift | 4 +- 6 files changed, 30 insertions(+), 41 deletions(-) diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index 7e6609add..1ee25e519 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -13,32 +13,30 @@ import UIKit import AppKit #endif -// TODO: try to make another internal instance that is isolated -// - possible make `ImageTask` a struct? then no questions about retaining it - /// 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, @unchecked Sendable { +@ImagePipelineActor +public final class ImageTask: Hashable { /// An identifier that uniquely identifies the task within a given pipeline. /// Unique only within that pipeline. - public let taskId: Int64 + public nonisolated let taskId: Int64 /// The original request that the task was created with. - public let request: ImageRequest + public nonisolated let request: ImageRequest /// The priority of the task. The priority can be updated dynamically even /// for a task that is already running. - public var priority: ImageRequest.Priority { + public nonisolated var priority: ImageRequest.Priority { get { nonisolatedState.withLock { $0.priority } } set { setPriority(newValue) } } /// Returns the current download progress. Returns zeros before the download /// is started and the expected size of the resource is known. - public var currentProgress: Progress { + public nonisolated var currentProgress: Progress { nonisolatedState.withLock { $0.progress } } @@ -62,7 +60,6 @@ public final class ImageTask: Hashable, @unchecked Sendable { } /// The current state of the task. - @ImagePipelineActor public private(set) var state: State = .running /// The state of the image task. @@ -76,7 +73,7 @@ public final class ImageTask: Hashable, @unchecked Sendable { } /// Returns `true` if the task cancellation is initiated. - public var isCancelling: Bool { + public nonisolated var isCancelling: Bool { nonisolatedState.withLock { $0.isCancelling } } @@ -101,7 +98,7 @@ public final class ImageTask: Hashable, @unchecked Sendable { } /// The stream of progress updates. - public var progress: AsyncStream { + public nonisolated var progress: AsyncStream { makeStream { if case .progress(let value) = $0 { return value } return nil @@ -112,7 +109,7 @@ public final class ImageTask: Hashable, @unchecked Sendable { /// progressive decoding. /// /// - seealso: ``ImagePipeline/Configuration-swift.struct/isProgressiveDecodingEnabled`` - public var previews: AsyncStream { + public nonisolated var previews: AsyncStream { makeStream { if case .preview(let value) = $0 { return value } return nil @@ -122,7 +119,7 @@ public final class ImageTask: Hashable, @unchecked Sendable { // MARK: - Events /// The events sent by the pipeline during the task execution. - public var events: AsyncStream { makeStream { $0 } } + public nonisolated var events: AsyncStream { makeStream { $0 } } /// An event produced during the runetime of the task. public enum Event: Sendable { @@ -141,29 +138,25 @@ public final class ImageTask: Hashable, @unchecked Sendable { private let nonisolatedState: Mutex private let isDataTask: Bool - private let onEvent: ((Event, ImageTask) -> Void)? - private var task: Task! + private let onEvent: (@Sendable (Event, ImageTask) -> Void)? + private nonisolated(unsafe) var task: Task! private weak var pipeline: ImagePipeline? - @ImagePipelineActor - var continuation: UnsafeContinuation? - - @ImagePipelineActor - var _events: PassthroughSubject? + private var continuation: UnsafeContinuation? + private var _events: PassthroughSubject? - init(taskId: Int64, request: ImageRequest, isDataTask: Bool, pipeline: ImagePipeline, onEvent: ((Event, ImageTask) -> Void)?) { + nonisolated init(taskId: Int64, request: ImageRequest, isDataTask: Bool, pipeline: ImagePipeline, onEvent: (@Sendable (Event, ImageTask) -> Void)?) { self.taskId = taskId self.request = request self.nonisolatedState = Mutex(ImageTaskState(priority: request.priority)) self.isDataTask = isDataTask self.pipeline = pipeline self.onEvent = onEvent - self.task = Task { + self.task = Task { @ImagePipelineActor in try await perform() } } - @ImagePipelineActor private func perform() async throws -> ImageResponse { try await withUnsafeThrowingContinuation { continuation = $0 @@ -181,7 +174,7 @@ public final class ImageTask: Hashable, @unchecked Sendable { /// /// The pipeline will immediately cancel any work associated with a task /// unless there is an equivalent outstanding task running. - public func cancel() { + public nonisolated func cancel() { guard nonisolatedState.withLock({ guard !$0.isCancelling else { return false } $0.isCancelling = true @@ -192,7 +185,7 @@ public final class ImageTask: Hashable, @unchecked Sendable { } } - private func setPriority(_ newValue: ImageRequest.Priority) { + private nonisolated func setPriority(_ newValue: ImageRequest.Priority) { guard nonisolatedState.withLock({ guard $0.priority != newValue else { return false } $0.priority = newValue @@ -207,7 +200,6 @@ public final class ImageTask: Hashable, @unchecked Sendable { /// Gets called when the task is cancelled either by the user or by an /// external event such as session invalidation. - @ImagePipelineActor func _cancel() { guard state == .running else { return } state = .cancelled @@ -215,7 +207,6 @@ public final class ImageTask: Hashable, @unchecked Sendable { } /// Gets called when the associated task sends a new event. - @ImagePipelineActor func process(_ event: AsyncTask.Event) { guard state == .running else { return } switch event { @@ -239,7 +230,6 @@ public final class ImageTask: Hashable, @unchecked Sendable { /// /// - warning: The task needs to be fully wired (`_continuation` present) /// before it can start sending the events. - @ImagePipelineActor private func dispatch(_ event: Event) { guard continuation != nil else { return // Task isn't fully wired yet @@ -263,11 +253,11 @@ public final class ImageTask: Hashable, @unchecked Sendable { // MARK: Hashable - public func hash(into hasher: inout Hasher) { + public nonisolated func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self).hashValue) } - public static func == (lhs: ImageTask, rhs: ImageTask) -> Bool { + public nonisolated static func == (lhs: ImageTask, rhs: ImageTask) -> Bool { ObjectIdentifier(lhs) == ObjectIdentifier(rhs) } } @@ -275,7 +265,7 @@ public final class ImageTask: Hashable, @unchecked Sendable { // MARK: - ImageTask (Private) extension ImageTask { - private func makeStream(of closure: @Sendable @escaping (Event) -> T?) -> AsyncStream { + private nonisolated func makeStream(of closure: @Sendable @escaping (Event) -> T?) -> AsyncStream { AsyncStream { continuation in Task { @ImagePipelineActor in guard state == .running else { @@ -301,7 +291,6 @@ extension ImageTask { } } - @ImagePipelineActor private func makeEvents() -> PassthroughSubject { if _events == nil { _events = PassthroughSubject() diff --git a/Sources/Nuke/Pipeline/ImagePipeline.swift b/Sources/Nuke/Pipeline/ImagePipeline.swift index 3645d75c6..326169b7e 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline.swift @@ -98,7 +98,7 @@ public final class ImagePipeline { Task { @ImagePipelineActor in guard !self.isInvalidated else { return } self.isInvalidated = true - self.tasks.keys.forEach(self.cancelImageTask) + self.tasks.keys.forEach { cancelImageTask($0) } } } @@ -141,7 +141,7 @@ public final class ImagePipeline { // MARK: - ImageTask (Internal) - nonisolated func makeImageTask(with request: ImageRequest, isDataTask: Bool = false, onEvent: ((ImageTask.Event, ImageTask) -> Void)? = nil) -> ImageTask { + nonisolated func makeImageTask(with request: ImageRequest, isDataTask: Bool = false, onEvent: (@Sendable (ImageTask.Event, ImageTask) -> Void)? = nil) -> ImageTask { let task = ImageTask(taskId: nextTaskId.incremented(), request: request, isDataTask: isDataTask, pipeline: self, onEvent: onEvent) delegate.imageTaskCreated(task, pipeline: self) return task diff --git a/Tests/MockDataLoader.swift b/Tests/MockDataLoader.swift index 367af57aa..e9979be38 100644 --- a/Tests/MockDataLoader.swift +++ b/Tests/MockDataLoader.swift @@ -26,7 +26,7 @@ class MockDataLoader: MockDataLoading, DataLoading, @unchecked Sendable { set { queue.isSuspended = newValue } } - func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> MockDataTaskProtocol { + func loadData(with request: URLRequest, didReceiveData: @Sendable @escaping (Data, URLResponse) -> Void, completion: @Sendable @escaping (Error?) -> Void) -> MockDataTaskProtocol { let task = MockDataTask() NotificationCenter.default.post(name: MockDataLoader.DidStartTask, object: self) @@ -64,7 +64,7 @@ class MockDataLoader: MockDataLoading, DataLoading, @unchecked Sendable { // Remove these and update to implement the actual protocol. protocol MockDataLoading: DataLoading { - func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> MockDataTaskProtocol + func loadData(with request: URLRequest, didReceiveData: @Sendable @escaping (Data, URLResponse) -> Void, completion: @Sendable @escaping (Error?) -> Void) -> MockDataTaskProtocol } extension MockDataLoading where Self: DataLoading { @@ -85,7 +85,7 @@ extension MockDataLoading where Self: DataLoading { } } -protocol MockDataTaskProtocol { +protocol MockDataTaskProtocol: Sendable { func cancel() } diff --git a/Tests/MockProgressiveDataLoader.swift b/Tests/MockProgressiveDataLoader.swift index 76c8f4a69..5ee65f035 100644 --- a/Tests/MockProgressiveDataLoader.swift +++ b/Tests/MockProgressiveDataLoader.swift @@ -26,7 +26,7 @@ final class MockProgressiveDataLoader: MockDataLoading, DataLoading, @unchecked self.chunks = Array(_createChunks(for: data, size: data.count / 3)) } - func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> MockDataTaskProtocol { + func loadData(with request: URLRequest, didReceiveData: @Sendable @escaping (Data, URLResponse) -> Void, completion: @Sendable @escaping (Error?) -> Void) -> MockDataTaskProtocol { self.didReceiveData = didReceiveData self.completion = completion self.resume() diff --git a/Tests/NukeTests/ImagePipelineTests/ImagePipelineResumableDataTests.swift b/Tests/NukeTests/ImagePipelineTests/ImagePipelineResumableDataTests.swift index 8d002fa6f..2186a9dc0 100644 --- a/Tests/NukeTests/ImagePipelineTests/ImagePipelineResumableDataTests.swift +++ b/Tests/NukeTests/ImagePipelineTests/ImagePipelineResumableDataTests.swift @@ -73,7 +73,7 @@ private class _MockResumableDataLoader: MockDataLoading, DataLoading, @unchecked } } - func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> MockDataTaskProtocol { + func loadData(with request: URLRequest, didReceiveData: @Sendable @escaping (Data, URLResponse) -> Void, completion: @Sendable @escaping (Error?) -> Void) -> MockDataTaskProtocol { let headers = request.allHTTPHeaderFields let completion = completion diff --git a/Tests/XCTestCase+Nuke.swift b/Tests/XCTestCase+Nuke.swift index 343cce2c8..22ff921d8 100644 --- a/Tests/XCTestCase+Nuke.swift +++ b/Tests/XCTestCase+Nuke.swift @@ -31,8 +31,8 @@ struct TestExpectationImagePipeline { @discardableResult func toLoadImage(with request: ImageRequest, - progress: ((_ intermediateResponse: ImageResponse?, _ completedUnitCount: Int64, _ totalUnitCount: Int64) -> Void)? = nil, - completion: ((Result) -> Void)? = nil) -> TestRecordedImageRequest { + progress: (@Sendable (_ intermediateResponse: ImageResponse?, _ completedUnitCount: Int64, _ totalUnitCount: Int64) -> Void)? = nil, + completion: (@Sendable (Result) -> Void)? = nil) -> TestRecordedImageRequest { let record = TestRecordedImageRequest() let expectation = test.expectation(description: "Image loaded for \(request)") record._task = pipeline.loadImage(with: request, progress: progress) { result in From b6209897cf01686e24dc989d3b22dccb5a5d3693 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 27 Oct 2024 10:51:54 -0400 Subject: [PATCH 55/73] Update how withLock is used --- Sources/Nuke/ImageTask.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Nuke/ImageTask.swift b/Sources/Nuke/ImageTask.swift index 1ee25e519..018f70e60 100644 --- a/Sources/Nuke/ImageTask.swift +++ b/Sources/Nuke/ImageTask.swift @@ -30,14 +30,14 @@ public final class ImageTask: Hashable { /// The priority of the task. The priority can be updated dynamically even /// for a task that is already running. public nonisolated var priority: ImageRequest.Priority { - get { nonisolatedState.withLock { $0.priority } } + get { nonisolatedState.withLock(\.priority) } set { setPriority(newValue) } } /// Returns the current download progress. Returns zeros before the download /// is started and the expected size of the resource is known. public nonisolated var currentProgress: Progress { - nonisolatedState.withLock { $0.progress } + nonisolatedState.withLock(\.progress) } /// The download progress. @@ -74,7 +74,7 @@ public final class ImageTask: Hashable { /// Returns `true` if the task cancellation is initiated. public nonisolated var isCancelling: Bool { - nonisolatedState.withLock { $0.isCancelling } + nonisolatedState.withLock(\.isCancelling) } // MARK: - Async/Await From 81e8412feb3e9625d7251a946b81580aa07a97fb Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 27 Oct 2024 10:55:35 -0400 Subject: [PATCH 56/73] ImageRequest.Container is no longer Sendable --- Sources/Nuke/ImageRequest.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Nuke/ImageRequest.swift b/Sources/Nuke/ImageRequest.swift index 4a8fed30e..cedc32812 100644 --- a/Sources/Nuke/ImageRequest.swift +++ b/Sources/Nuke/ImageRequest.swift @@ -28,7 +28,7 @@ import AppKit /// ) /// let image = try await pipeline.image(for: request) /// ``` -public struct ImageRequest: CustomStringConvertible, Sendable, ExpressibleByStringLiteral { +public struct ImageRequest: CustomStringConvertible, @unchecked Sendable, ExpressibleByStringLiteral { // MARK: Options /// The relative priority of the request. The priority affects the order in @@ -437,7 +437,7 @@ public struct ImageRequest: CustomStringConvertible, Sendable, ExpressibleByStri 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 { + private final class Container { // 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. From 072f73277ab4942003480b0834273ca303f28100 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 27 Oct 2024 11:37:12 -0400 Subject: [PATCH 57/73] Update ci.yml --- .github/workflows/ci.yml | 73 +++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f60e8d6a7..e79f7024f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,22 +10,22 @@ on: jobs: ios-latest: - name: Unit Tests (iOS 17.4, Xcode 15.3) - runs-on: macOS-14 + name: Unit Tests (iOS 18.0, Xcode 16.0) + runs-on: macOS-15 env: - DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer steps: - uses: actions/checkout@v2 - name: Run Tests run: | - .scripts/test.sh -s "Nuke" -d "OS=17.4,name=iPhone 15 Pro" - .scripts/test.sh -s "NukeUI" -d "OS=17.4,name=iPhone 15 Pro" - .scripts/test.sh -s "NukeExtensions" -d "OS=17.4,name=iPhone 15 Pro" + .scripts/test.sh -s "Nuke" -d "OS=18.0,name=iPhone 16 Pro" + .scripts/test.sh -s "NukeUI" -d "OS=18.0,name=iPhone 16 Pro" + .scripts/test.sh -s "NukeExtensions" -d "OS=18.0,name=iPhone 16 Pro" macos-latest: - name: Unit Tests (macOS, Xcode 15.3) - runs-on: macOS-14 + name: Unit Tests (macOS, Xcode 16.0) + runs-on: macOS-15 env: - DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer steps: - uses: actions/checkout@v2 - name: Run Tests @@ -34,17 +34,17 @@ jobs: .scripts/test.sh -s "NukeUI" -d "platform=macOS" .scripts/test.sh -s "NukeExtensions" -d "platform=macOS" tvos-latest: - name: Unit Tests (tvOS 17.4, Xcode 15.3) - runs-on: macOS-14 + name: Unit Tests (tvOS 18.0, Xcode 16.0) + runs-on: macOS-15 env: - DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer steps: - uses: actions/checkout@v2 - name: Run Tests run: | - .scripts/test.sh -s "Nuke" -d "OS=17.4,name=Apple TV" - .scripts/test.sh -s "NukeUI" -d "OS=17.4,name=Apple TV" - .scripts/test.sh -s "NukeExtensions" -d "OS=17.4,name=Apple TV" + .scripts/test.sh -s "Nuke" -d "OS=18.0,name=Apple TV" + .scripts/test.sh -s "NukeUI" -d "OS=18.0,name=Apple TV" + .scripts/test.sh -s "NukeExtensions" -d "OS=18.0,name=Apple TV" # There is a problem with watchOS runners where they often fail to launch on CI # # watchos-latest: @@ -59,27 +59,30 @@ jobs: # .scripts/test.sh -s "Nuke" -d "OS=9.1,name=Apple Watch Series 8 (45mm)" # .scripts/test.sh -s "NukeUI" -d "OS=9.1,name=Apple Watch Series 8 (45mm)" # .scripts/test.sh -s "Nuke Extensions" -d "OS=9.1,name=Apple Watch Series 8 (45mm)" - ios-xcode-14-3-1: - name: Unit Tests (iOS 17.0, Xcode 15.0) - runs-on: macOS-13 - env: - DEVELOPER_DIR: /Applications/Xcode_15.0.app/Contents/Developer - steps: - - uses: actions/checkout@v2 - - name: Run Tests - run: | - .scripts/test.sh -s "Nuke" -d "OS=17.0,name=iPhone 15 Pro" - .scripts/test.sh -s "NukeUI" -d "OS=17.0,name=iPhone 15 Pro" - .scripts/test.sh -s "NukeExtensions" -d "OS=17.0,name=iPhone 15 Pro" + +# Nuke 13.0 supports only the latest version of Xcode (16). +# +# ios-xcode-14-3-1: +# name: Unit Tests (iOS 17.0, Xcode 15.0) +# runs-on: macOS-13 +# env: +# DEVELOPER_DIR: /Applications/Xcode_15.0.app/Contents/Developer +# steps: +# - uses: actions/checkout@v2 +# - name: Run Tests +# run: | +# .scripts/test.sh -s "Nuke" -d "OS=17.0,name=iPhone 15 Pro" +# .scripts/test.sh -s "NukeUI" -d "OS=17.0,name=iPhone 15 Pro" +# .scripts/test.sh -s "NukeExtensions" -d "OS=17.0,name=iPhone 15 Pro" ios-thread-safety: name: Thread Safety Tests (TSan Enabled) - runs-on: macOS-14 + runs-on: macOS-15 env: - DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer steps: - uses: actions/checkout@v2 - name: Run Tests - run: .scripts/test.sh -s "Nuke Thread Safety Tests" -d "OS=17.4,name=iPhone 15 Pro" + run: .scripts/test.sh -s "Nuke Thread Safety Tests" -d "OS=18.0,name=iPhone 16 Pro" # ios-memory-management-tests: # name: Memory Management Tests # runs-on: macOS-13 @@ -91,18 +94,18 @@ jobs: # run: .scripts/test.sh -s "Nuke Memory Management Tests" -d "OS=14.4,name=iPhone 12 Pro" ios-performance-tests: name: Performance Tests - runs-on: macOS-14 + runs-on: macOS-15 env: - DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer steps: - uses: actions/checkout@v2 - name: Run Tests - run: .scripts/test.sh -s "Nuke Performance Tests" -d "OS=17.4,name=iPhone 15 Pro" + run: .scripts/test.sh -s "Nuke Performance Tests" -d "OS=18.0,name=iPhone 16 Pro" swift-build: name: Swift Build (SPM) - runs-on: macOS-14 + runs-on: macOS-15 env: - DEVELOPER_DIR: /Applications/Xcode_15.3.app/Contents/Developer + DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer steps: - uses: actions/checkout@v2 - name: Build From 9c025c6090e9b0c019da65f107e5500f74432d25 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 27 Oct 2024 12:04:53 -0400 Subject: [PATCH 58/73] Update performance tests --- Tests/NukePerformanceTests/ImagePipelinePerformanceTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/NukePerformanceTests/ImagePipelinePerformanceTests.swift b/Tests/NukePerformanceTests/ImagePipelinePerformanceTests.swift index f7431c22e..772cc6f7a 100644 --- a/Tests/NukePerformanceTests/ImagePipelinePerformanceTests.swift +++ b/Tests/NukePerformanceTests/ImagePipelinePerformanceTests.swift @@ -18,7 +18,7 @@ class ImagePipelinePerfomanceTests: XCTestCase { var finished: Int = 0 let semaphore = DispatchSemaphore(value: 0) for request in requests { - pipeline.loadImage(with: request, queue: callbackQueue, progress: nil) { _ in + pipeline.loadImage(with: request, progress: nil) { _ in finished += 1 if finished == requests.count { semaphore.signal() From a4413f67ddc8c96948d88463ce21dce0ce80da04 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 27 Oct 2024 12:15:47 -0400 Subject: [PATCH 59/73] Bump macOS deployment target to macOS 11 --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index e41ee56c3..002ca85b6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 import PackageDescription let package = Package( @@ -6,7 +6,7 @@ let package = Package( platforms: [ .iOS(.v13), .tvOS(.v13), - .macOS(.v10_15), + .macOS(.v11), .watchOS(.v6), .visionOS(.v1), ], From de39ec1f0d67348d26ab6674c86185462899ca4d Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 27 Oct 2024 12:20:08 -0400 Subject: [PATCH 60/73] Increase deployment targets --- Package.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Package.swift b/Package.swift index 002ca85b6..b8217f1d4 100644 --- a/Package.swift +++ b/Package.swift @@ -4,10 +4,10 @@ import PackageDescription let package = Package( name: "Nuke", platforms: [ - .iOS(.v13), - .tvOS(.v13), + .iOS(.v14), + .tvOS(.v14), .macOS(.v11), - .watchOS(.v6), + .watchOS(.v7), .visionOS(.v1), ], products: [ From 4878846229bb9d900c1e0e80678f1897669d362a Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 27 Oct 2024 12:23:37 -0400 Subject: [PATCH 61/73] Remove @unchecked from ImagePipeline.Error Sendable conformance --- Sources/Nuke/Pipeline/ImagePipeline+Error.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Nuke/Pipeline/ImagePipeline+Error.swift b/Sources/Nuke/Pipeline/ImagePipeline+Error.swift index 0e0152a3d..fe42f0e99 100644 --- a/Sources/Nuke/Pipeline/ImagePipeline+Error.swift +++ b/Sources/Nuke/Pipeline/ImagePipeline+Error.swift @@ -6,7 +6,7 @@ import Foundation extension ImagePipeline { /// Represents all possible image pipeline errors. - public enum Error: Swift.Error, CustomStringConvertible, @unchecked Sendable { + public enum Error: Swift.Error, CustomStringConvertible, 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. From 28967695f406a98a2e167ff0f95163693c944ed4 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 17 Nov 2024 08:10:07 -0500 Subject: [PATCH 62/73] Fix SwiftPM build on macOS --- Sources/Nuke/Encoding/ImageEncoding.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/Nuke/Encoding/ImageEncoding.swift b/Sources/Nuke/Encoding/ImageEncoding.swift index 276245555..749c982da 100644 --- a/Sources/Nuke/Encoding/ImageEncoding.swift +++ b/Sources/Nuke/Encoding/ImageEncoding.swift @@ -32,8 +32,10 @@ extension ImageEncoding { } } +// note: @unchecked was added to surpress build errors with NSImage on macOS + /// Image encoding context used when selecting which encoder to use. -public struct ImageEncodingContext: Sendable { +public struct ImageEncodingContext: @unchecked Sendable { public let request: ImageRequest public let image: PlatformImage public let urlResponse: URLResponse? From 7e684d13e313276edcb36477bb951645eff6c907 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 17 Nov 2024 08:16:30 -0500 Subject: [PATCH 63/73] Add retroactive conformances in test targets --- Tests/NukeExtensions.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/NukeExtensions.swift b/Tests/NukeExtensions.swift index 873c4a894..38cf8fe91 100644 --- a/Tests/NukeExtensions.swift +++ b/Tests/NukeExtensions.swift @@ -5,7 +5,7 @@ import Foundation import Nuke -extension ImagePipeline.Error: Equatable { +extension ImagePipeline.Error: @retroactive Equatable { public static func == (lhs: ImagePipeline.Error, rhs: ImagePipeline.Error) -> Bool { switch (lhs, rhs) { case (.dataMissingInCache, .dataMissingInCache): return true @@ -22,7 +22,7 @@ extension ImagePipeline.Error: Equatable { } } -extension ImageResponse: Equatable { +extension ImageResponse: @retroactive Equatable { public static func == (lhs: ImageResponse, rhs: ImageResponse) -> Bool { return lhs.image === rhs.image } From ef82a7d18355563ed9a928b8009225cbc772beca Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 17 Nov 2024 08:19:59 -0500 Subject: [PATCH 64/73] Enable Swift 6 --- Nuke.xcodeproj/project.pbxproj | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Nuke.xcodeproj/project.pbxproj b/Nuke.xcodeproj/project.pbxproj index 6a2d923d1..ba7f4085d 100644 --- a/Nuke.xcodeproj/project.pbxproj +++ b/Nuke.xcodeproj/project.pbxproj @@ -1875,6 +1875,7 @@ OTHER_SWIFT_FLAGS = "-D DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.nukeui-unit-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -1891,6 +1892,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.nukeui-unit-tests"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -1907,6 +1909,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.Nuke-Thread-Safety-Tests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -1922,6 +1925,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.Nuke-Thread-Safety-Tests"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -1984,6 +1988,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.NukeExtensionsTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -2004,6 +2009,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.NukeExtensionsTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -2061,6 +2067,7 @@ OTHER_SWIFT_FLAGS = "-D DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.Nuke-Tests"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -2076,6 +2083,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.Nuke-Tests"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -2140,6 +2148,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Nuke Tests Host.app/Nuke Tests Host"; }; name = Debug; @@ -2161,6 +2170,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Nuke Tests Host.app/Nuke Tests Host"; }; name = Release; @@ -2222,7 +2232,7 @@ SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos"; SUPPORTS_MACCATALYST = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TVOS_DEPLOYMENT_TARGET = 13.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -2279,7 +2289,7 @@ SKIP_INSTALL = YES; SUPPORTED_PLATFORMS = "watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos"; SUPPORTS_MACCATALYST = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TVOS_DEPLOYMENT_TARGET = 13.0; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; From bec2101b77c2c4e4cab6f129d79cf045a3753dfa Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 17 Nov 2024 09:20:44 -0500 Subject: [PATCH 65/73] Update RateLimiterTests to swift-testing --- Tests/NukeTests/RateLimiterTests.swift | 66 ++++++++------------------ 1 file changed, 19 insertions(+), 47 deletions(-) diff --git a/Tests/NukeTests/RateLimiterTests.swift b/Tests/NukeTests/RateLimiterTests.swift index c60afd52a..93741f342 100644 --- a/Tests/NukeTests/RateLimiterTests.swift +++ b/Tests/NukeTests/RateLimiterTests.swift @@ -2,74 +2,46 @@ // // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). -import XCTest @testable import Nuke +import Testing -class RateLimiterTests: XCTestCase { - var rateLimiter: RateLimiter! +@Suite @ImagePipelineActor struct RateLimiterTests { + let rateLimiter = RateLimiter(rate: 10, burst: 2) - override func setUp() { - super.setUp() - - // Note: we set very short rate to avoid bucket form being refilled too quickly - rateLimiter = RateLimiter(rate: 10, burst: 2) - } - - @ImagePipelineActor - func testThatBurstIsExecutedimmediately() { - // Given + @Test func burstIsExecutedImmediately() throws { var isExecuted = Array(repeating: false, count: 4) - - // When for i in isExecuted.indices { rateLimiter.execute { isExecuted[i] = true return true } } - - // Then - XCTAssertEqual(isExecuted, [true, true, false, false], "Expect first 2 items to be executed immediately") + try #require(isExecuted == [true, true, false, false], "Expect first 2 items to be executed immediately") } - @ImagePipelineActor - func testThatNotExecutedItemDoesntExtractFromBucket() { - // Given + @Test func NotExecutedItemDoesNotExtractFromBucket() throws { var isExecuted = Array(repeating: false, count: 4) - - // When for i in isExecuted.indices { rateLimiter.execute { isExecuted[i] = true return i != 1 // important! } } - - // Then - XCTAssertEqual(isExecuted, [true, true, true, false], "Expect first 2 items to be executed immediately") + try #require(isExecuted == [true, true, true, false], "Expect first 2 items to be executed immediately") } - - @ImagePipelineActor - func testOverflow() { - // Given - var isExecuted = Array(repeating: false, count: 3) - - // When - let expectation = self.expectation(description: "All work executed") - expectation.expectedFulfillmentCount = isExecuted.count - - for i in isExecuted.indices { - rateLimiter.execute { - isExecuted[i] = true - expectation.fulfill() - return true + + @Test func overflow() async throws { + let count = 3 + await confirmation(expectedCount: count) { done in + for _ in 0.. Date: Sun, 17 Nov 2024 09:33:53 -0500 Subject: [PATCH 66/73] Use expect instead of require --- Tests/NukeTests/RateLimiterTests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/NukeTests/RateLimiterTests.swift b/Tests/NukeTests/RateLimiterTests.swift index 93741f342..13316c1bf 100644 --- a/Tests/NukeTests/RateLimiterTests.swift +++ b/Tests/NukeTests/RateLimiterTests.swift @@ -8,7 +8,7 @@ import Testing @Suite @ImagePipelineActor struct RateLimiterTests { let rateLimiter = RateLimiter(rate: 10, burst: 2) - @Test func burstIsExecutedImmediately() throws { + @Test func burstIsExecutedImmediately() { var isExecuted = Array(repeating: false, count: 4) for i in isExecuted.indices { rateLimiter.execute { @@ -16,10 +16,10 @@ import Testing return true } } - try #require(isExecuted == [true, true, false, false], "Expect first 2 items to be executed immediately") + #expect(isExecuted == [true, true, false, false], "Expect first 2 items to be executed immediately") } - @Test func NotExecutedItemDoesNotExtractFromBucket() throws { + @Test func posponedItemsDoNotExtractFromBucket() { var isExecuted = Array(repeating: false, count: 4) for i in isExecuted.indices { rateLimiter.execute { @@ -27,10 +27,10 @@ import Testing return i != 1 // important! } } - try #require(isExecuted == [true, true, true, false], "Expect first 2 items to be executed immediately") + #expect(isExecuted == [true, true, true, false], "Expect first 2 items to be executed immediately") } - @Test func overflow() async throws { + @Test func overflow() async { let count = 3 await confirmation(expectedCount: count) { done in for _ in 0.. Date: Sun, 17 Nov 2024 12:41:19 -0500 Subject: [PATCH 67/73] Enable complete concurrency checking in unit tests --- Nuke.xcodeproj/project.pbxproj | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Nuke.xcodeproj/project.pbxproj b/Nuke.xcodeproj/project.pbxproj index ba7f4085d..bddc5a873 100644 --- a/Nuke.xcodeproj/project.pbxproj +++ b/Nuke.xcodeproj/project.pbxproj @@ -1837,7 +1837,6 @@ OTHER_SWIFT_FLAGS = "-D DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukeui; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_STRICT_CONCURRENCY = complete; }; name = Debug; }; @@ -1857,7 +1856,6 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukeui; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_STRICT_CONCURRENCY = complete; }; name = Release; }; @@ -1946,7 +1944,6 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.nuke-extensions"; PRODUCT_NAME = NukeExtensions; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_STRICT_CONCURRENCY = complete; }; name = Debug; }; @@ -1966,7 +1963,6 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.nuke-extensions"; PRODUCT_NAME = NukeExtensions; - SWIFT_STRICT_CONCURRENCY = complete; }; name = Release; }; @@ -2030,7 +2026,6 @@ OTHER_SWIFT_FLAGS = "-D DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukevideo; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_STRICT_CONCURRENCY = complete; }; name = Debug; }; @@ -2050,7 +2045,6 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nukevideo; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_STRICT_CONCURRENCY = complete; }; name = Release; }; @@ -2067,6 +2061,7 @@ OTHER_SWIFT_FLAGS = "-D DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.Nuke-Tests"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Debug; @@ -2083,6 +2078,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = "com.github.kean.Nuke-Tests"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_STRICT_CONCURRENCY = complete; SWIFT_VERSION = 5.0; }; name = Release; @@ -2315,7 +2311,6 @@ OTHER_SWIFT_FLAGS = "-D DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nuke; PRODUCT_NAME = Nuke; - SWIFT_STRICT_CONCURRENCY = complete; }; name = Debug; }; @@ -2335,7 +2330,6 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.github.kean.nuke; PRODUCT_NAME = Nuke; - SWIFT_STRICT_CONCURRENCY = complete; }; name = Release; }; From 13d363e60958348f1ca498bb2e72333a4472477f Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 17 Nov 2024 12:52:21 -0500 Subject: [PATCH 68/73] Update TaskTests --- Tests/NukeTests/RateLimiterTests.swift | 2 +- Tests/NukeTests/TaskTests.swift | 148 ++++++++++++------------- 2 files changed, 74 insertions(+), 76 deletions(-) diff --git a/Tests/NukeTests/RateLimiterTests.swift b/Tests/NukeTests/RateLimiterTests.swift index 13316c1bf..2c002ad1b 100644 --- a/Tests/NukeTests/RateLimiterTests.swift +++ b/Tests/NukeTests/RateLimiterTests.swift @@ -2,8 +2,8 @@ // // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). -@testable import Nuke import Testing +@testable import Nuke @Suite @ImagePipelineActor struct RateLimiterTests { let rateLimiter = RateLimiter(rate: 10, burst: 2) diff --git a/Tests/NukeTests/TaskTests.swift b/Tests/NukeTests/TaskTests.swift index 51e962fa6..22e5e6a56 100644 --- a/Tests/NukeTests/TaskTests.swift +++ b/Tests/NukeTests/TaskTests.swift @@ -2,14 +2,14 @@ // // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). -import XCTest +import Testing +import Foundation @testable import Nuke -@ImagePipelineActor -class TaskTests: XCTestCase { +@Suite @ImagePipelineActor struct TaskTests { // MARK: - Starter - func testStarterCalledOnFirstSubscription() { + @Test func starterCalledOnFirstSubscription() { // Given var startCount = 0 _ = SimpleTask(starter: { _ in @@ -17,10 +17,10 @@ class TaskTests: XCTestCase { }) // Then - XCTAssertEqual(startCount, 0) + #expect(startCount == 0) } - func testStarterCalledWhenSubscriptionIsAdded() { + @Test func starterCalledWhenSubscriptionIsAdded() { // Given var startCount = 0 let task = SimpleTask(starter: { _ in @@ -31,10 +31,10 @@ class TaskTests: XCTestCase { _ = task.subscribe { _ in } // Then started is called - XCTAssertEqual(startCount, 1) + #expect(startCount == 1) } - func testStarterOnlyCalledOnce() { + @Test func starterOnlyCalledOnce() { // Given var startCount = 0 let task = SimpleTask(starter: { _ in @@ -46,10 +46,10 @@ class TaskTests: XCTestCase { _ = task.subscribe { _ in } // Then started is only called once - XCTAssertEqual(startCount, 1) + #expect(startCount == 1) } - func testStarterIsDeallocated() { + @Test func tarterIsDeallocated() { // Given class Foo { } @@ -64,18 +64,18 @@ class TaskTests: XCTestCase { }) } - XCTAssertNotNil(weakFoo, "Foo is retained by starter") + #expect(weakFoo != nil, "Foo is retained by starter") // When first subscription is added and starter is called _ = task.subscribe { _ in } // Then - XCTAssertNil(weakFoo, "Started wasn't deallocated") + #expect(weakFoo == nil, "Started wasn't deallocated") } // MARK: - Subscribe - func testWhenSubscriptionAddedEventsAreForwarded() { + @Test func whenSubscriptionAddedEventsAreForwarded() { // Given let task = SimpleTask(starter: { $0.send(progress: TaskProgress(completed: 1, total: 2)) @@ -91,7 +91,7 @@ class TaskTests: XCTestCase { } // Then - XCTAssertEqual(recordedEvents, [ + #expect(recordedEvents == [ .progress(TaskProgress(completed: 1, total: 2)), .value(1, isCompleted: false), .progress(TaskProgress(completed: 2, total: 2)), @@ -99,7 +99,7 @@ class TaskTests: XCTestCase { ]) } - func testBothSubscriptionsReceiveEvents() { + @Test func bothSubscriptionsReceiveEvents() { // Given let task = AsyncTask() @@ -107,20 +107,20 @@ class TaskTests: XCTestCase { var eventCount = 0 _ = task.subscribe { event in - XCTAssertEqual(event, .value(1, isCompleted: false)) + #expect(event == .value(1, isCompleted: false)) eventCount += 1 } _ = task.subscribe { event in - XCTAssertEqual(event, .value(1, isCompleted: false)) + #expect(event == .value(1, isCompleted: false)) eventCount += 1 } task.send(value: 1) // Then - XCTAssertEqual(eventCount, 2) + #expect(eventCount == 2) } - func testCantSubscribeToAlreadyCancelledTask() { + @Test func cantSubscribeToAlreadyCancelledTask() { // Given let task = SimpleTask(starter: { _ in }) let subscription = task.subscribe { _ in } @@ -129,10 +129,10 @@ class TaskTests: XCTestCase { subscription?.unsubscribe() // Then - XCTAssertNil(task.subscribe { _ in }) + #expect(task.subscribe { _ in } == nil) } - func testCantSubscribeToAlreadySucceededTask() { + @Test func cantSubscribeToAlreadySucceededTask() { // Given let task = AsyncTask() _ = task.subscribe { _ in } @@ -141,10 +141,10 @@ class TaskTests: XCTestCase { task.send(value: 1, isCompleted: true) // Then - XCTAssertNil(task.subscribe { _ in }) + #expect(task.subscribe { _ in } == nil) } - func testCantSubscribeToAlreadyFailedTasks() { + @Test func cantSubscribeToAlreadyFailedTasks() { // Given let task = AsyncTask() _ = task.subscribe { _ in } @@ -153,29 +153,27 @@ class TaskTests: XCTestCase { task.send(error: .init(raw: "1")) // Then - XCTAssertNil(task.subscribe { _ in }) + #expect(task.subscribe { _ in } == nil) } - func testSubscribeToTaskWithSynchronousCompletionReturnsNil() { + @Test func subscribeToTaskWithSynchronousCompletionReturnsNil() async { // Given let task = SimpleTask { (task) in task.send(value: 0, isCompleted: true) } - // When - let expectation = self.expectation(description: "Observer called") - let subscription = task.subscribe { _ in - expectation.fulfill() + // When/Then + await withUnsafeContinuation { continuation in + let subscription = task.subscribe { _ in + continuation.resume() + } + #expect(subscription == nil) } - - // Then - XCTAssertNil(subscription) - wait() } // MARK: - Ubsubscribe - func testWhenSubscriptionIsRemovedNoEventsAreSent() { + @Test func whenSubscriptionIsRemovedNoEventsAreSent() { // Given let task = AsyncTask() var recordedEvents = [AsyncTask.Event]() @@ -186,10 +184,10 @@ class TaskTests: XCTestCase { task.send(value: 1) // Then - XCTAssertTrue(recordedEvents.isEmpty, "Expect no events to be received by observer after subscription is removed") + #expect(recordedEvents.isEmpty, "Expect no events to be received by observer after subscription is removed") } - func testWhenSubscriptionIsRemovedTaskBecomesDisposed() { + @Test func whenSubscriptionIsRemovedTaskBecomesDisposed() { // Given let task = AsyncTask() let subscription = task.subscribe { _ in } @@ -198,10 +196,10 @@ class TaskTests: XCTestCase { subscription?.unsubscribe() // Then - XCTAssertTrue(task.isDisposed, "Expect task to be marked as disposed") + #expect(task.isDisposed, "Expect task to be marked as disposed") } - func testWhenSubscriptionIsRemovedOnCancelIsCalled() { + @Test func whenSubscriptionIsRemovedOnCancelIsCalled() { // Given let task = AsyncTask() let subscription = task.subscribe { _ in } @@ -215,39 +213,39 @@ class TaskTests: XCTestCase { subscription?.unsubscribe() // Then - XCTAssertTrue(onCancelledIsCalled) + #expect(onCancelledIsCalled) } - func testWhenSubscriptionIsRemovedOperationIsCancelled() { + @Test func whenSubscriptionIsRemovedOperationIsCancelled() { // Given let operation = Foundation.Operation() let task = SimpleTask(starter: { $0.operation = operation }) let subscription = task.subscribe { _ in } - XCTAssertFalse(operation.isCancelled) + #expect(!operation.isCancelled) // When subscription?.unsubscribe() // Then - XCTAssertTrue(operation.isCancelled) + #expect(operation.isCancelled) } - func testWhenSubscriptionIsRemovedDependencyIsCancelled() { + @Test func whenSubscriptionIsRemovedDependencyIsCancelled() { // Given let operation = Foundation.Operation() let dependency = SimpleTask(starter: { $0.operation = operation }) let task = SimpleTask(starter: { $0.dependency = dependency.subscribe { _ in } }) let subscription = task.subscribe { _ in } - XCTAssertFalse(operation.isCancelled) + #expect(!operation.isCancelled) // When subscription?.unsubscribe() // Then - XCTAssertTrue(operation.isCancelled) + #expect(operation.isCancelled) } - func testWhenOneOfTwoSubscriptionsAreRemovedTaskNotCancelled() { + @Test func whenOneOfTwoSubscriptionsAreRemovedTaskNotCancelled() { // Given let operation = Foundation.Operation() let task = SimpleTask(starter: { $0.operation = operation }) @@ -258,10 +256,10 @@ class TaskTests: XCTestCase { subscription1?.unsubscribe() // Then - XCTAssertFalse(operation.isCancelled) + #expect(!operation.isCancelled) } - func testWhenTwoOfTwoSubscriptionsAreRemovedTaskIsCancelled() { + @Test func whenTwoOfTwoSubscriptionsAreRemovedTaskIsCancelled() { // Given let operation = Foundation.Operation() let task = SimpleTask(starter: { $0.operation = operation }) @@ -273,12 +271,12 @@ class TaskTests: XCTestCase { subscription2?.unsubscribe() // Then - XCTAssertTrue(operation.isCancelled) + #expect(operation.isCancelled) } // MARK: - Priority - func testWhenPriorityIsUpdatedOperationPriorityAlsoUpdated() { + @Test func whenPriorityIsUpdatedOperationPriorityAlsoUpdated() { // Given let operation = Foundation.Operation() let task = SimpleTask(starter: { $0.operation = operation }) @@ -288,10 +286,10 @@ class TaskTests: XCTestCase { subscription?.setPriority(.high) // Then - XCTAssertEqual(operation.queuePriority, .high) + #expect(operation.queuePriority == .high) } - func testWhenTaskChangesOperationPriorityUpdated() { // Or sets operation later + @Test func whenTaskChangesOperationPriorityUpdated() { // Or sets operation later // Given let task = AsyncTask() let subscription = task.subscribe { _ in } @@ -302,10 +300,10 @@ class TaskTests: XCTestCase { task.operation = operation // Then - XCTAssertEqual(operation.queuePriority, .high) + #expect(operation.queuePriority == .high) } - func testThatPriorityCanBeLowered() { + @Test func priorityCanBeLowered() { // Given let operation = Foundation.Operation() let task = SimpleTask(starter: { $0.operation = operation }) @@ -315,10 +313,10 @@ class TaskTests: XCTestCase { subscription?.setPriority(.low) // Then - XCTAssertEqual(operation.queuePriority, .low) + #expect(operation.queuePriority == .low) } - func testThatPriorityEqualMaximumPriorityOfAllSubscriptions() { + @Test func priorityEqualMaximumPriorityOfAllSubscriptions() { // Given let operation = Foundation.Operation() let task = SimpleTask(starter: { $0.operation = operation }) @@ -330,10 +328,10 @@ class TaskTests: XCTestCase { subscription2?.setPriority(.high) // Then - XCTAssertEqual(operation.queuePriority, .high) + #expect(operation.queuePriority == .high) } - func testWhenSubscriptionIsRemovedPriorityIsUpdated() { + @Test func subscriptionIsRemovedPriorityIsUpdated() { // Given let operation = Foundation.Operation() let task = SimpleTask(starter: { $0.operation = operation }) @@ -347,10 +345,10 @@ class TaskTests: XCTestCase { subscription2?.unsubscribe() // Then - XCTAssertEqual(operation.queuePriority, .low) + #expect(operation.queuePriority == .low) } - func testWhenSubscriptionLowersPriorityButExistingSubscriptionHasHigherPriporty() { + @Test func whenSubscriptionLowersPriorityButExistingSubscriptionHasHigherPriporty() { // Given let operation = Foundation.Operation() let task = SimpleTask(starter: { $0.operation = operation }) @@ -362,10 +360,10 @@ class TaskTests: XCTestCase { subscription1?.setPriority(.low) // Then order of updating sub - XCTAssertEqual(operation.queuePriority, .high) + #expect(operation.queuePriority == .high) } - func testPriorityOfDependencyUpdated() { + @Test func priorityOfDependencyUpdated() { // Given let operation = Foundation.Operation() let dependency = SimpleTask(starter: { $0.operation = operation }) @@ -376,12 +374,12 @@ class TaskTests: XCTestCase { subscription?.setPriority(.high) // Then - XCTAssertEqual(operation.queuePriority, .high) + #expect(operation.queuePriority == .high) } // MARK: - Dispose - func testExecutingTaskIsntDisposed() { + @Test func executingTaskIsntDisposed() { // Given let task = AsyncTask() var isDisposeCalled = false @@ -392,11 +390,11 @@ class TaskTests: XCTestCase { task.send(value: 1) // Casually sending value // Then - XCTAssertFalse(isDisposeCalled) - XCTAssertFalse(task.isDisposed) + #expect(!isDisposeCalled) + #expect(!task.isDisposed) } - func testThatTaskIsDisposedWhenCancelled() { + @Test func taskIsDisposedWhenCancelled() { // Given let task = SimpleTask(starter: { _ in }) var isDisposeCalled = false @@ -407,11 +405,11 @@ class TaskTests: XCTestCase { subscription?.unsubscribe() // Then - XCTAssertTrue(isDisposeCalled) - XCTAssertTrue(task.isDisposed) + #expect(isDisposeCalled) + #expect(task.isDisposed) } - func testThatTaskIsDisposedWhenCompletedWithSuccess() { + @Test func taskIsDisposedWhenCompletedWithSuccess() { // Given let task = AsyncTask() var isDisposeCalled = false @@ -422,11 +420,11 @@ class TaskTests: XCTestCase { task.send(value: 1, isCompleted: true) // Then - XCTAssertTrue(isDisposeCalled) - XCTAssertTrue(task.isDisposed) + #expect(isDisposeCalled) + #expect(task.isDisposed) } - func testThatTaskIsDisposedWhenCompletedWithFailure() { + @Test func taskIsDisposedWhenCompletedWithFailure() { // Given let task = AsyncTask() var isDisposeCalled = false @@ -437,8 +435,8 @@ class TaskTests: XCTestCase { task.send(error: .init(raw: "1")) // Then - XCTAssertTrue(isDisposeCalled) - XCTAssertTrue(task.isDisposed) + #expect(isDisposeCalled) + #expect(task.isDisposed) } } From 52771a052bcd78ec7afe586943cf9414bdc613c0 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 17 Nov 2024 13:32:07 -0500 Subject: [PATCH 69/73] Update ImageRequestTests --- Tests/NukeTests/ImageRequestTests.swift | 135 ++++++++++++------------ 1 file changed, 68 insertions(+), 67 deletions(-) diff --git a/Tests/NukeTests/ImageRequestTests.swift b/Tests/NukeTests/ImageRequestTests.swift index 48e65302c..3377acae1 100644 --- a/Tests/NukeTests/ImageRequestTests.swift +++ b/Tests/NukeTests/ImageRequestTests.swift @@ -2,12 +2,13 @@ // // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). -import XCTest +import Testing +import Foundation @testable import Nuke -class ImageRequestTests: XCTestCase { +@Suite struct ImageRequestTests { // The compiler picks up the new version - func testInit() { + @Test func testInit() { _ = ImageRequest(url: Test.url) _ = ImageRequest(url: Test.url, processors: []) _ = ImageRequest(url: Test.url, processors: []) @@ -15,13 +16,13 @@ class ImageRequestTests: XCTestCase { _ = ImageRequest(url: Test.url, options: [.reloadIgnoringCachedData]) } - func testExpressibleByStringLiteral() { + @Test func expressibleByStringLiteral() { let _: ImageRequest = "https://example.com/image.jpeg" } // MARK: - CoW - func testCopyOnWrite() { + @Test func copyOnWrite() { // GIVEN var request = ImageRequest(url: URL(string: "http://test.com/1.png")) request.options.insert(.disableMemoryCacheReads) @@ -35,165 +36,165 @@ class ImageRequestTests: XCTestCase { copy.priority = .low // THEN - XCTAssertEqual(copy.options.contains(.disableMemoryCacheReads), true) - XCTAssertEqual(copy.userInfo["key"] as? String, "3") - XCTAssertEqual((copy.processors.first as? MockImageProcessor)?.identifier, "4") - XCTAssertEqual(request.priority, .high) // Original request no updated - XCTAssertEqual(copy.priority, .low) + #expect(copy.options.contains(.disableMemoryCacheReads) == true) + #expect(copy.userInfo["key"] as? String == "3") + #expect((copy.processors.first as? MockImageProcessor)?.identifier == "4") + #expect(request.priority == .high) // Original request no updated // Original request no updated + #expect(copy.priority == .low) } // MARK: - Misc // Just to make sure that comparison works as expected. - func testPriorityComparison() { + @Test func priorityComparison() { typealias Priority = ImageRequest.Priority - XCTAssertTrue(Priority.veryLow < Priority.veryHigh) - XCTAssertTrue(Priority.low < Priority.normal) - XCTAssertTrue(Priority.normal == Priority.normal) + #expect(Priority.veryLow < Priority.veryHigh) + #expect(Priority.low < Priority.normal) + #expect(Priority.normal == Priority.normal) } - func testUserInfoKey() { + @Test func userInfoKey() { // WHEN let request = ImageRequest(url: Test.url, userInfo: [.init("a"): 1]) // THEN - XCTAssertNotNil(request.userInfo["a"]) + #expect(request.userInfo["a"] != nil) } } -class ImageRequestCacheKeyTests: XCTestCase { - func testDefaults() { +@Suite struct ImageRequestCacheKeyTests { + @Test func defaults() { let request = Test.request - AssertHashableEqual(MemoryCacheKey(request), MemoryCacheKey(request)) // equal to itself + expectHashableMatch(MemoryCacheKey(request), MemoryCacheKey(request)) // equal to itself } - func testRequestsWithTheSameURLsAreEquivalent() { + @Test func requestsWithTheSameURLsAreEquivalent() { let lhs = ImageRequest(url: Test.url) let rhs = ImageRequest(url: Test.url) - AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) + expectHashableMatch(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } - func testRequestsWithDefaultURLRequestAndURLAreEquivalent() { + @Test func requestsWithDefaultURLRequestAndURLAreEquivalent() { let lhs = ImageRequest(url: Test.url) let rhs = ImageRequest(urlRequest: URLRequest(url: Test.url)) - AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) + expectHashableMatch(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } - func testRequestsWithDifferentURLsAreNotEquivalent() { + @Test func requestsWithDifferentURLsAreNotEquivalent() { let lhs = ImageRequest(url: URL(string: "http://test.com/1.png")) let rhs = ImageRequest(url: URL(string: "http://test.com/2.png")) - XCTAssertNotEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) + #expect(MemoryCacheKey(lhs) != MemoryCacheKey(rhs)) } - func testRequestsWithTheSameProcessorsAreEquivalent() { + @Test func requestsWithTheSameProcessorsAreEquivalent() { let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) - AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) + expectHashableMatch(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } - func testRequestsWithDifferentProcessorsAreNotEquivalent() { + @Test func requestsWithDifferentProcessorsAreNotEquivalent() { let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "2")]) - XCTAssertNotEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) + #expect(MemoryCacheKey(lhs) != MemoryCacheKey(rhs)) } - func testURLRequestParametersAreIgnored() { + @Test func uRLRequestParametersAreIgnored() { let lhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .reloadRevalidatingCacheData, timeoutInterval: 50)) let rhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 0)) - AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) + expectHashableMatch(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } - func testSettingDefaultProcessorManually() { + @Test func settingDefaultProcessorManually() { let lhs = ImageRequest(url: Test.url) let rhs = ImageRequest(url: Test.url, processors: lhs.processors) - AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) + expectHashableMatch(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } } -class ImageRequestLoadKeyTests: XCTestCase { - func testDefaults() { +@Suite struct ImageRequestLoadKeyTests { + @Test func defaults() { let request = ImageRequest(url: Test.url) - AssertHashableEqual(TaskFetchOriginalDataKey(request), TaskFetchOriginalDataKey(request)) + expectHashableMatch(TaskFetchOriginalDataKey(request), TaskFetchOriginalDataKey(request)) } - func testRequestsWithTheSameURLsAreEquivalent() { + @Test func requestsWithTheSameURLsAreEquivalent() { let lhs = ImageRequest(url: Test.url) let rhs = ImageRequest(url: Test.url) - AssertHashableEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs)) + expectHashableMatch(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs)) } - func testRequestsWithDifferentURLsAreNotEquivalent() { + @Test func requestsWithDifferentURLsAreNotEquivalent() { let lhs = ImageRequest(url: URL(string: "http://test.com/1.png")) let rhs = ImageRequest(url: URL(string: "http://test.com/2.png")) - XCTAssertNotEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs)) + #expect(TaskFetchOriginalDataKey(lhs) != TaskFetchOriginalDataKey(rhs)) } - func testRequestsWithTheSameProcessorsAreEquivalent() { + @Test func requestsWithTheSameProcessorsAreEquivalent() { let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) - AssertHashableEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs)) + expectHashableMatch(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs)) } - func testRequestsWithDifferentProcessorsAreEquivalent() { + @Test func requestsWithDifferentProcessorsAreEquivalent() { let lhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "1")]) let rhs = ImageRequest(url: Test.url, processors: [MockImageProcessor(id: "2")]) - AssertHashableEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs)) + expectHashableMatch(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs)) } - func testRequestWithDifferentURLRequestParametersAreNotEquivalent() { + @Test func requestWithDifferentURLRequestParametersAreNotEquivalent() { let lhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .reloadRevalidatingCacheData, timeoutInterval: 50)) let rhs = ImageRequest(urlRequest: URLRequest(url: Test.url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 0)) - XCTAssertNotEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs)) + #expect(TaskFetchOriginalDataKey(lhs) != TaskFetchOriginalDataKey(rhs)) } - func testMockImageProcessorCorrectlyImplementsIdentifiers() { - XCTAssertEqual(MockImageProcessor(id: "1").identifier, MockImageProcessor(id: "1").identifier) - XCTAssertEqual(MockImageProcessor(id: "1").hashableIdentifier, MockImageProcessor(id: "1").hashableIdentifier) + @Test func mockImageProcessorCorrectlyImplementsIdentifiers() { + #expect(MockImageProcessor(id: "1").identifier == MockImageProcessor(id: "1").identifier) + #expect(MockImageProcessor(id: "1").hashableIdentifier == MockImageProcessor(id: "1").hashableIdentifier) - XCTAssertNotEqual(MockImageProcessor(id: "1").identifier, MockImageProcessor(id: "2").identifier) - XCTAssertNotEqual(MockImageProcessor(id: "1").hashableIdentifier, MockImageProcessor(id: "2").hashableIdentifier) + #expect(MockImageProcessor(id: "1").identifier != MockImageProcessor(id: "2").identifier) + #expect(MockImageProcessor(id: "1").hashableIdentifier != MockImageProcessor(id: "2").hashableIdentifier) } } -class ImageRequestImageIdTests: XCTestCase { - func testThatCacheKeyUsesAbsoluteURLByDefault() { +@Suite struct ImageRequestImageIdTests { + @Test func thatCacheKeyUsesAbsoluteURLByDefault() { let lhs = ImageRequest(url: Test.url) let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1")) - XCTAssertNotEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) + #expect(MemoryCacheKey(lhs) != MemoryCacheKey(rhs)) } - func testThatCacheKeyUsesFilteredURLWhenSet() { + @Test func thatCacheKeyUsesFilteredURLWhenSet() { let lhs = ImageRequest(url: Test.url, userInfo: [.imageIdKey: Test.url.absoluteString]) let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1"), userInfo: [.imageIdKey: Test.url.absoluteString]) - AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) + expectHashableMatch(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } - func testThatCacheKeyForProcessedImageDataUsesAbsoluteURLByDefault() { + @Test func thatCacheKeyForProcessedImageDataUsesAbsoluteURLByDefault() { let lhs = ImageRequest(url: Test.url) let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1")) - XCTAssertNotEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) + #expect(MemoryCacheKey(lhs) != MemoryCacheKey(rhs)) } - func testThatCacheKeyForProcessedImageDataUsesFilteredURLWhenSet() { + @Test func thatCacheKeyForProcessedImageDataUsesFilteredURLWhenSet() { let lhs = ImageRequest(url: Test.url, userInfo: [.imageIdKey: Test.url.absoluteString]) let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1"), userInfo: [.imageIdKey: Test.url.absoluteString]) - AssertHashableEqual(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) + expectHashableMatch(MemoryCacheKey(lhs), MemoryCacheKey(rhs)) } - func testThatLoadKeyForProcessedImageDoesntUseFilteredURL() { + @Test func thatLoadKeyForProcessedImageDoesntUseFilteredURL() { let lhs = ImageRequest(url: Test.url, userInfo: [.imageIdKey: Test.url.absoluteString]) let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1"), userInfo: [.imageIdKey: Test.url.absoluteString]) - XCTAssertNotEqual(TaskLoadImageKey(lhs), TaskLoadImageKey(rhs)) + #expect(TaskLoadImageKey(lhs) != TaskLoadImageKey(rhs)) } - func testThatLoadKeyForOriginalImageDoesntUseFilteredURL() { + @Test func thatLoadKeyForOriginalImageDoesntUseFilteredURL() { let lhs = ImageRequest(url: Test.url, userInfo: [.imageIdKey: Test.url.absoluteString]) let rhs = ImageRequest(url: Test.url.appendingPathComponent("?token=1"), userInfo: [.imageIdKey: Test.url.absoluteString]) - XCTAssertNotEqual(TaskFetchOriginalDataKey(lhs), TaskFetchOriginalDataKey(rhs)) + #expect(TaskFetchOriginalDataKey(lhs) != TaskFetchOriginalDataKey(rhs)) } } -private func AssertHashableEqual(_ lhs: T, _ rhs: T, file: StaticString = #file, line: UInt = #line) { - XCTAssertEqual(lhs.hashValue, rhs.hashValue, file: file, line: line) - XCTAssertEqual(lhs, rhs, file: file, line: line) +private func expectHashableMatch(_ lhs: T, _ rhs: T) { + #expect(lhs.hashValue == rhs.hashValue) + #expect(lhs == rhs) } From 4e0bca9161cfff4df46e230e9e1e5da845f23560 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 17 Nov 2024 13:39:19 -0500 Subject: [PATCH 70/73] Update ImageCacheTests --- Tests/NukeTests/ImageCacheTests.swift | 409 ++++++++++++-------------- 1 file changed, 192 insertions(+), 217 deletions(-) diff --git a/Tests/NukeTests/ImageCacheTests.swift b/Tests/NukeTests/ImageCacheTests.swift index 788b9925a..6b5ecc396 100644 --- a/Tests/NukeTests/ImageCacheTests.swift +++ b/Tests/NukeTests/ImageCacheTests.swift @@ -2,422 +2,397 @@ // // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). -import XCTest +import Foundation +import Testing @testable import Nuke -private func _request(index: Int) -> ImageRequest { - return ImageRequest(url: URL(string: "http://example.com/img\(index)")!) -} -private let request1 = _request(index: 1) -private let request2 = _request(index: 2) -private let request3 = _request(index: 3) +#if canImport(UIKit) +import UIKit +#endif -class ImageCacheTests: XCTestCase, @unchecked Sendable { - var cache: ImageCache! - - override func setUp() { - super.setUp() - - cache = ImageCache() +@Suite struct ImageCacheTests { + let cache = ImageCache() + + init() { cache.entryCostLimit = 2 } - + // MARK: - Basics - - @MainActor - func testCacheCreation() { - XCTAssertEqual(cache.totalCount, 0) - XCTAssertNil(cache[Test.request]) + + @Test func cacheCreation() { + #expect(cache.totalCount == 0) + #expect(cache[Test.request] == nil) } - - @MainActor - func testThatImageIsStored() { + + @Test func imageIsStored() { // When cache[Test.request] = Test.container - + // Then - XCTAssertEqual(cache.totalCount, 1) - XCTAssertNotNil(cache[Test.request]) + #expect(cache.totalCount == 1) + #expect(cache[Test.request] != nil) } - + // MARK: - Subscript - - @MainActor - func testThatImageIsStoredUsingSubscript() { + + @Test func imageIsStoredUsingSubscript() { // When cache[Test.request] = Test.container - + // Then - XCTAssertNotNil(cache[Test.request]) + #expect(cache[Test.request] != nil) } - + // MARK: - Count - - @MainActor - func testThatTotalCountChanges() { - XCTAssertEqual(cache.totalCount, 0) - + + @Test func totalCountChanges() { + #expect(cache.totalCount == 0) + cache[request1] = Test.container - XCTAssertEqual(cache.totalCount, 1) - + #expect(cache.totalCount == 1) + cache[request2] = Test.container - XCTAssertEqual(cache.totalCount, 2) - + #expect(cache.totalCount == 2) + cache[request2] = nil - XCTAssertEqual(cache.totalCount, 1) - + #expect(cache.totalCount == 1) + cache[request1] = nil - XCTAssertEqual(cache.totalCount, 0) + #expect(cache.totalCount == 0) } - - @MainActor - func testThatCountLimitChanges() { + + @Test func countLimitChanges() { // When cache.countLimit = 1 - + // Then - XCTAssertEqual(cache.countLimit, 1) + #expect(cache.countLimit == 1) } - - @MainActor - func testThatTTLChanges() { - //when + + @Test func ttlChanges() { + // when cache.ttl = 1 - + // Then - XCTAssertEqual(cache.ttl, 1) + #expect(cache.ttl == 1) } - - @MainActor - func testThatItemsAreRemoveImmediatelyWhenCountLimitIsReached() { + + @Test func itemsAreRemoveImmediatelyWhenCountLimitIsReached() { // Given cache.countLimit = 1 - + // When cache[request1] = Test.container cache[request2] = Test.container - + // Then - XCTAssertNil(cache[request1]) - XCTAssertNotNil(cache[request2]) + #expect(cache[request1] == nil) + #expect(cache[request2] != nil) } - - @MainActor - func testTrimToCount() { + + @Test func trimToCount() { // Given cache[request1] = Test.container cache[request2] = Test.container - + // When cache.trim(toCount: 1) - + // Then - XCTAssertNil(cache[request1]) - XCTAssertNotNil(cache[request2]) + #expect(cache[request1] == nil) + #expect(cache[request2] != nil) } - - @MainActor - func testThatImagesAreRemovedOnCountLimitChange() { + + @Test func imagesAreRemovedOnCountLimitChange() { // Given cache.countLimit = 2 - + cache[request1] = Test.container cache[request2] = Test.container - + // When cache.countLimit = 1 - + // Then - XCTAssertNil(cache[request1]) - XCTAssertNotNil(cache[request2]) + #expect(cache[request1] == nil) + #expect(cache[request2] != nil) } - + // MARK: Cost - -#if !os(macOS) - - @MainActor - func testDefaultImageCost() { - XCTAssertEqual(cache.cost(for: ImageContainer(image: Test.image)), 1228800) + +#if canImport(UIKit) + + @Test func defaultImageCost() { + #expect(cache.cost(for: ImageContainer(image: Test.image)) == 1228800) } - - @MainActor - func testThatTotalCostChanges() { + + @Test func totalCostChanges() { let imageCost = cache.cost(for: ImageContainer(image: Test.image)) - XCTAssertEqual(cache.totalCost, 0) - + #expect(cache.totalCost == 0) + cache[request1] = Test.container - XCTAssertEqual(cache.totalCost, imageCost) - + #expect(cache.totalCost == imageCost) + cache[request2] = Test.container - XCTAssertEqual(cache.totalCost, 2 * imageCost) - + #expect(cache.totalCost == 2 * imageCost) + cache[request2] = nil - XCTAssertEqual(cache.totalCost, imageCost) - + #expect(cache.totalCost == imageCost) + cache[request1] = nil - XCTAssertEqual(cache.totalCost, 0) + #expect(cache.totalCost == 0) } - - @MainActor - func testThatCostLimitChanged() { + + @Test func costLimitChanged() { // Given let cost = cache.cost(for: ImageContainer(image: Test.image)) - + // When cache.costLimit = Int(Double(cost) * 1.5) - + // Then - XCTAssertEqual(cache.costLimit, Int(Double(cost) * 1.5)) + #expect(cache.costLimit == Int(Double(cost) * 1.5)) } - - @MainActor - func testThatItemsAreRemoveImmediatelyWhenCostLimitIsReached() { + + @Test func itemsAreRemoveImmediatelyWhenCostLimitIsReached() { // Given let cost = cache.cost(for: ImageContainer(image: Test.image)) cache.costLimit = Int(Double(cost) * 1.5) - + // When/Then cache[request1] = Test.container - + // LRU item is released cache[request2] = Test.container - XCTAssertNil(cache[request1]) - XCTAssertNotNil(cache[request2]) + #expect(cache[request1] == nil) + #expect(cache[request2] != nil) } - - @MainActor - func testEntryCostLimitEntryStored() { + + @Test func entryCostLimitEntryStored() { // Given let container = ImageContainer(image: Test.image) let cost = cache.cost(for: container) cache.costLimit = Int(Double(cost) * 15) cache.entryCostLimit = 0.1 - + // When cache[Test.request] = container - + // Then - XCTAssertNotNil(cache[Test.request]) - XCTAssertEqual(cache.totalCount, 1) + #expect(cache[Test.request] != nil) + #expect(cache.totalCount == 1) } - - @MainActor - func testEntryCostLimitEntryNotStored() { + + @Test func entryCostLimitEntryNotStored() { // Given let container = ImageContainer(image: Test.image) let cost = cache.cost(for: container) cache.costLimit = Int(Double(cost) * 3) cache.entryCostLimit = 0.1 - + // When cache[Test.request] = container - + // Then - XCTAssertNil(cache[Test.request]) - XCTAssertEqual(cache.totalCount, 0) + #expect(cache[Test.request] == nil) + #expect(cache.totalCount == 0) } - - @MainActor - func testTrimToCost() { + + @Test func trimToCost() { // Given cache.costLimit = Int.max - + cache[request1] = Test.container cache[request2] = Test.container - + // When let cost = cache.cost(for: ImageContainer(image: Test.image)) cache.trim(toCost: Int(Double(cost) * 1.5)) - + // Then - XCTAssertNil(cache[request1]) - XCTAssertNotNil(cache[request2]) + #expect(cache[request1] == nil) + #expect(cache[request2] != nil) } - - @MainActor - func testThatImagesAreRemovedOnCostLimitChange() { + + @Test func imagesAreRemovedOnCostLimitChange() { // Given let cost = cache.cost(for: ImageContainer(image: Test.image)) cache.costLimit = Int(Double(cost) * 2.5) - + cache[request1] = Test.container cache[request2] = Test.container - + // When cache.costLimit = cost - + // Then - XCTAssertNil(cache[request1]) - XCTAssertNotNil(cache[request2]) + #expect(cache[request1] == nil) + #expect(cache[request2] != nil) } - - @MainActor - func testImageContainerWithoutAssociatedDataCost() { + + @Test func imageContainerWithoutAssociatedDataCost() { // Given let data = Test.data(name: "cat", extension: "gif") let image = PlatformImage(data: data)! let container = ImageContainer(image: image, data: nil) - + // Then - XCTAssertEqual(cache.cost(for: container), 558000) + #expect(cache.cost(for: container) == 558000) } - - @MainActor - func testImageContainerWithAssociatedDataCost() { + + @Test func imageContainerWithAssociatedDataCost() { // Given let data = Test.data(name: "cat", extension: "gif") let image = PlatformImage(data: data)! let container = ImageContainer(image: image, data: data) - + // Then - XCTAssertEqual(cache.cost(for: container), 558000 + 427672) + #expect(cache.cost(for: container) == 558000 + 427672) } - + #endif - + // MARK: LRU - - @MainActor - func testThatLeastRecentItemsAreRemoved() { + + @Test func leastRecentItemsAreRemoved() { // Given let cost = cache.cost(for: ImageContainer(image: Test.image)) cache.costLimit = Int(Double(cost) * 2.5) - + cache[request1] = Test.container cache[request2] = Test.container cache[request3] = Test.container - + // Then - XCTAssertNil(cache[request1]) - XCTAssertNotNil(cache[request2]) - XCTAssertNotNil(cache[request3]) + #expect(cache[request1] == nil) + #expect(cache[request2] != nil) + #expect(cache[request3] != nil) } - - @MainActor - func testThatItemsAreTouched() { + + @Test func itemsAreTouched() { // Given let cost = cache.cost(for: ImageContainer(image: Test.image)) cache.costLimit = Int(Double(cost) * 2.5) - + cache[request1] = Test.container cache[request2] = Test.container _ = cache[request1] // Touched image - + // When cache[request3] = Test.container - + // Then - XCTAssertNotNil(cache[request1]) - XCTAssertNil(cache[request2]) - XCTAssertNotNil(cache[request3]) + #expect(cache[request1] != nil) + #expect(cache[request2] == nil) + #expect(cache[request3] != nil) } - + // MARK: Misc - - @MainActor - func testRemoveAll() { + + @Test func removeAll() { // GIVEN cache[request1] = Test.container cache[request2] = Test.container - + // WHEN cache.removeAll() - + // THEN - XCTAssertEqual(cache.totalCount, 0) - XCTAssertEqual(cache.totalCost, 0) + #expect(cache.totalCount == 0) + #expect(cache.totalCost == 0) } - -#if os(iOS) || os(tvOS) || os(visionOS) - @MainActor - func testThatSomeImagesAreRemovedOnDidEnterBackground() async { + +#if canImport(UIKit) + @Test @MainActor func someImagesAreRemovedOnDidEnterBackground() async { // GIVEN cache.costLimit = Int.max cache.countLimit = 10 // 1 out of 10 images should remain - + for i in 0..<10 { cache[_request(index: i)] = Test.container } - XCTAssertEqual(cache.totalCount, 10) - + #expect(cache.totalCount == 10) + // WHEN let task = Task { @MainActor in NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) - + // THEN - XCTAssertEqual(cache.totalCount, 1) + #expect(cache.totalCount == 1) } await task.value } - - @MainActor - func testThatSomeImagesAreRemovedBasedOnCostOnDidEnterBackground() async { + + @Test @MainActor func someImagesAreRemovedBasedOnCostOnDidEnterBackground() async { // GIVEN let cost = cache.cost(for: ImageContainer(image: Test.image)) cache.costLimit = cost * 10 cache.countLimit = Int.max - + for index in 0..<10 { let request = ImageRequest(url: URL(string: "http://example.com/img\(index)")!) cache[request] = Test.container } - XCTAssertEqual(cache.totalCount, 10) - + #expect(cache.totalCount == 10) + // WHEN let task = Task { @MainActor in NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil) - + // THEN - XCTAssertEqual(cache.totalCount, 1) + #expect(cache.totalCount == 1) } await task.value } #endif } -class InternalCacheTTLTests: XCTestCase { +@Suite struct InternalCacheTTLTests { let cache = Cache(costLimit: 1000, countLimit: 1000) - + // MARK: TTL - - @MainActor - func testTTL() { + + @Test func ttl() { // Given cache.set(1, forKey: 1, cost: 1, ttl: 0.05) // 50 ms - XCTAssertNotNil(cache.value(forKey: 1)) - + #expect(cache.value(forKey: 1) != nil) + // When usleep(55 * 1000) - + // Then - XCTAssertNil(cache.value(forKey: 1)) + #expect(cache.value(forKey: 1) == nil) } - - @MainActor - func testDefaultTTLIsUsed() { + + @Test func defaultTTLIsUsed() { // Given cache.conf.ttl = 0.05// 50 ms cache.set(1, forKey: 1, cost: 1) - XCTAssertNotNil(cache.value(forKey: 1)) - + #expect(cache.value(forKey: 1) != nil) + // When usleep(55 * 1000) - + // Then - XCTAssertNil(cache.value(forKey: 1)) + #expect(cache.value(forKey: 1) == nil) } - - @MainActor - func testDefaultToNonExpiringEntries() { + + @Test func defaultToNonExpiringEntries() { // Given cache.set(1, forKey: 1, cost: 1) - XCTAssertNotNil(cache.value(forKey: 1)) - + #expect(cache.value(forKey: 1) != nil) + // When usleep(55 * 1000) - + // Then - XCTAssertNotNil(cache.value(forKey: 1)) + #expect(cache.value(forKey: 1) != nil) } } + +private func _request(index: Int) -> ImageRequest { + return ImageRequest(url: URL(string: "http://example.com/img\(index)")!) +} +private let request1 = _request(index: 1) +private let request2 = _request(index: 2) +private let request3 = _request(index: 3) From b62cc4346a97e6fe89fe94d24fedcf7d28c85301 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 17 Nov 2024 14:04:47 -0500 Subject: [PATCH 71/73] Update ImageDecoderTests --- Sources/Nuke/Caching/Cache.swift | 6 +- Tests/NukeTests/ImageDecoderTests.swift | 277 ++++++++++++------------ 2 files changed, 142 insertions(+), 141 deletions(-) diff --git a/Sources/Nuke/Caching/Cache.swift b/Sources/Nuke/Caching/Cache.swift index f38f9e86e..d8ac82e25 100644 --- a/Sources/Nuke/Caching/Cache.swift +++ b/Sources/Nuke/Caching/Cache.swift @@ -56,8 +56,8 @@ final class Cache: @unchecked Sendable { self.memoryPressure.resume() #if os(iOS) || os(tvOS) || os(visionOS) - Task { - await registerForEnterBackground() + Task { @MainActor in + registerForEnterBackground() } #endif } @@ -70,7 +70,7 @@ final class Cache: @unchecked Sendable { } #if os(iOS) || os(tvOS) || os(visionOS) - @MainActor private func registerForEnterBackground() { + private func registerForEnterBackground() { notificationObserver = NotificationCenter.default.addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in self?.clearCacheOnEnterBackground() } diff --git a/Tests/NukeTests/ImageDecoderTests.swift b/Tests/NukeTests/ImageDecoderTests.swift index 74420bb09..6d22f4b40 100644 --- a/Tests/NukeTests/ImageDecoderTests.swift +++ b/Tests/NukeTests/ImageDecoderTests.swift @@ -2,237 +2,238 @@ // // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). -import XCTest +import Foundation +import Testing @testable import Nuke -class ImageDecoderTests: XCTestCase { - func testDecodePNG() throws { +@Suite struct ImageDecoderTests { + @Test func decodePNG() throws { // Given let data = Test.data(name: "fixture", extension: "png") let decoder = ImageDecoders.Default() - + // When - let container = try XCTUnwrap(decoder.decode(data)) - + let container = try #require(try decoder.decode(data)) + // Then - XCTAssertEqual(container.type, .png) - XCTAssertFalse(container.isPreview) - XCTAssertNil(container.data) - XCTAssertTrue(container.userInfo.isEmpty) + #expect(container.type == .png) + #expect(!container.isPreview) + #expect(container.data == nil) + #expect(container.userInfo.isEmpty) } - - func testDecodeJPEG() throws { + + @Test func decodeJPEG() throws { // Given let data = Test.data(name: "baseline", extension: "jpeg") let decoder = ImageDecoders.Default() - + // When - let container = try XCTUnwrap(decoder.decode(data)) - + let container = try #require(try decoder.decode(data)) + // Then - XCTAssertEqual(container.type, .jpeg) - XCTAssertFalse(container.isPreview) - XCTAssertNil(container.data) - XCTAssertTrue(container.userInfo.isEmpty) + #expect(container.type == .jpeg) + #expect(!container.isPreview) + #expect(container.data == nil) + #expect(container.userInfo.isEmpty) } - - func testDecodingProgressiveJPEG() { + + @Test func decodingProgressiveJPEG() { let data = Test.data(name: "progressive", extension: "jpeg") let decoder = ImageDecoders.Default() - + // Just before the Start Of Frame - XCTAssertNil(decoder.decodePartiallyDownloadedData(data[0...358])) - XCTAssertEqual(decoder.numberOfScans, 0) - + #expect(decoder.decodePartiallyDownloadedData(data[0...358]) == nil) + #expect(decoder.numberOfScans == 0) + // Right after the Start Of Frame - XCTAssertNil(decoder.decodePartiallyDownloadedData(data[0...359])) - XCTAssertEqual(decoder.numberOfScans, 0) // still haven't finished the first scan - + #expect(decoder.decodePartiallyDownloadedData(data[0...359]) == nil) + #expect(decoder.numberOfScans == 0) // still haven't finished the first scan // still haven't finished the first scan + // Just before the first Start Of Scan - XCTAssertNil(decoder.decodePartiallyDownloadedData(data[0...438])) - XCTAssertEqual(decoder.numberOfScans, 0) // still haven't finished the first scan - + #expect(decoder.decodePartiallyDownloadedData(data[0...438]) == nil) + #expect(decoder.numberOfScans == 0) // still haven't finished the first scan // still haven't finished the first scan + // Found the first Start Of Scan - XCTAssertNil(decoder.decodePartiallyDownloadedData(data[0...439])) - XCTAssertEqual(decoder.numberOfScans, 1) - + #expect(decoder.decodePartiallyDownloadedData(data[0...439]) == nil) + #expect(decoder.numberOfScans == 1) + // Found the second Start of Scan let scan1 = decoder.decodePartiallyDownloadedData(data[0...2952]) - XCTAssertNotNil(scan1) - XCTAssertEqual(scan1?.isPreview, true) + #expect(scan1 != nil) + #expect(scan1?.isPreview == true) if let image = scan1?.image { #if os(macOS) - XCTAssertEqual(image.size.width, 450) - XCTAssertEqual(image.size.height, 300) + #expect(image.size.width == 450) + #expect(image.size.height == 300) #else - XCTAssertEqual(image.size.width * image.scale, 450) - XCTAssertEqual(image.size.height * image.scale, 300) + #expect(image.size.width * image.scale == 450) + #expect(image.size.height * image.scale == 300) #endif } - XCTAssertEqual(decoder.numberOfScans, 2) - XCTAssertEqual(scan1?.userInfo[.scanNumberKey] as? Int, 2) - + #expect(decoder.numberOfScans == 2) + #expect(scan1?.userInfo[.scanNumberKey] as? Int == 2) + // Feed all data and see how many scans are there // In practice the moment we finish receiving data we call // `decode(data: data, isCompleted: true)` so we might not scan all the // of the bytes and encounter all of the scans (e.g. the final chunk // of data that we receive contains multiple scans). - XCTAssertNotNil(decoder.decodePartiallyDownloadedData(data)) - XCTAssertEqual(decoder.numberOfScans, 10) + #expect(decoder.decodePartiallyDownloadedData(data) != nil) + #expect(decoder.numberOfScans == 10) } - - func testDecodeGIF() throws { + + @Test func decodeGIF() throws { // Given let data = Test.data(name: "cat", extension: "gif") let decoder = ImageDecoders.Default() - + // When - let container = try XCTUnwrap(decoder.decode(data)) - + let container = try #require(try decoder.decode(data)) + // Then - XCTAssertEqual(container.type, .gif) - XCTAssertFalse(container.isPreview) - XCTAssertNotNil(container.data) - XCTAssertTrue(container.userInfo.isEmpty) + #expect(container.type == .gif) + #expect(!container.isPreview) + #expect(container.data != nil) + #expect(container.userInfo.isEmpty) } - - func testDecodeHEIC() throws { + + @Test func decodeHEIC() throws { // Given let data = Test.data(name: "img_751", extension: "heic") let decoder = ImageDecoders.Default() - + // When - let container = try XCTUnwrap(decoder.decode(data)) - + let container = try #require(try decoder.decode(data)) + // Then - XCTAssertNil(container.type) // TODO: update when HEIF support is added - XCTAssertFalse(container.isPreview) - XCTAssertNil(container.data) - XCTAssertTrue(container.userInfo.isEmpty) + #expect(container.type == nil) // TODO: update when HEIF support is added // TODO: update when HEIF support is added + #expect(!container.isPreview) + #expect(container.data == nil) + #expect(container.userInfo.isEmpty) } - - func testDecodingGIFDataAttached() throws { + + @Test func decodingGIFDataAttached() throws { let data = Test.data(name: "cat", extension: "gif") - XCTAssertNotNil(try ImageDecoders.Default().decode(data).data) + #expect(try ImageDecoders.Default().decode(data).data != nil) } - - func testDecodingGIFPreview() throws { + + @Test func decodingGIFPreview() throws { let data = Test.data(name: "cat", extension: "gif") - XCTAssertEqual(data.count, 427672) // 427 KB + #expect(data.count == 427672) // 427 KB // 427 KB let chunk = data[...60000] // 6 KB let response = try ImageDecoders.Default().decode(chunk) - XCTAssertEqual(response.image.sizeInPixels, CGSize(width: 500, height: 279)) + #expect(response.image.sizeInPixels == CGSize(width: 500, height: 279)) } - - func testDecodingGIFPreviewGeneratedOnlyOnce() throws { + + @Test func decodingGIFPreviewGeneratedOnlyOnce() throws { let data = Test.data(name: "cat", extension: "gif") - XCTAssertEqual(data.count, 427672) // 427 KB + #expect(data.count == 427672) // 427 KB // 427 KB let chunk = data[...60000] // 6 KB - + let context = ImageDecodingContext.mock(data: chunk) - let decoder = try XCTUnwrap(ImageDecoders.Default(context: context)) - - XCTAssertNotNil(decoder.decodePartiallyDownloadedData(chunk)) - XCTAssertNil(decoder.decodePartiallyDownloadedData(chunk)) + let decoder = try #require(ImageDecoders.Default(context: context)) + + #expect(decoder.decodePartiallyDownloadedData(chunk) != nil) + #expect(decoder.decodePartiallyDownloadedData(chunk) == nil) } - - func testDecodingPNGDataNotAttached() throws { + + @Test func decodingPNGDataNotAttached() throws { let data = Test.data(name: "fixture", extension: "png") let container = try ImageDecoders.Default().decode(data) - XCTAssertNil(container.data) + #expect(container.data == nil) } - + #if os(iOS) || os(tvOS) || os(macOS) || os(visionOS) - func testDecodeBaselineWebP() throws { + @Test func decodeBaselineWebP() throws { if #available(OSX 11.0, iOS 14.0, watchOS 7.0, tvOS 999.0, *) { let data = Test.data(name: "baseline", extension: "webp") let container = try ImageDecoders.Default().decode(data) - XCTAssertEqual(container.image.sizeInPixels, CGSize(width: 550, height: 368)) - XCTAssertNil(container.data) + #expect(container.image.sizeInPixels == CGSize(width: 550, height: 368)) + #expect(container.data == nil) } } #endif } -class ImageTypeTests: XCTestCase { +@Suite struct ImageTypeTests { // MARK: PNG - - func testDetectPNG() { + + @Test func detectPNG() { let data = Test.data(name: "fixture", extension: "png") - XCTAssertNil(AssetType(data[0..<1])) - XCTAssertNil(AssetType(data[0..<7])) - XCTAssertEqual(AssetType(data[0..<8]), .png) - XCTAssertEqual(AssetType(data), .png) + #expect(AssetType(data[0..<1]) == nil) + #expect(AssetType(data[0..<7]) == nil) + #expect(AssetType(data[0..<8]) == .png) + #expect(AssetType(data) == .png) } - + // MARK: GIF - - func testDetectGIF() { + + @Test func detectGIF() { let data = Test.data(name: "cat", extension: "gif") - XCTAssertEqual(AssetType(data), .gif) + #expect(AssetType(data) == .gif) } - + // MARK: JPEG - - func testDetectBaselineJPEG() { + + @Test func detectBaselineJPEG() { let data = Test.data(name: "baseline", extension: "jpeg") - XCTAssertNil(AssetType(data[0..<1])) - XCTAssertNil(AssetType(data[0..<2])) - XCTAssertEqual(AssetType(data[0..<3]), .jpeg) - XCTAssertEqual(AssetType(data), .jpeg) + #expect(AssetType(data[0..<1]) == nil) + #expect(AssetType(data[0..<2]) == nil) + #expect(AssetType(data[0..<3]) == .jpeg) + #expect(AssetType(data) == .jpeg) } - - func testDetectProgressiveJPEG() { + + @Test func detectProgressiveJPEG() { let data = Test.data(name: "progressive", extension: "jpeg") // Not enough data - XCTAssertNil(AssetType(Data())) - XCTAssertNil(AssetType(data[0..<2])) - + #expect(AssetType(Data()) == nil) + #expect(AssetType(data[0..<2]) == nil) + // Enough to determine image format - XCTAssertEqual(AssetType(data[0..<3]), .jpeg) - XCTAssertEqual(AssetType(data[0..<33]), .jpeg) - + #expect(AssetType(data[0..<3]) == .jpeg) + #expect(AssetType(data[0..<33]) == .jpeg) + // Full image - XCTAssertEqual(AssetType(data), .jpeg) + #expect(AssetType(data) == .jpeg) } - + // MARK: WebP - - func testDetectBaselineWebP() { + + @Test func detectBaselineWebP() { let data = Test.data(name: "baseline", extension: "webp") - XCTAssertNil(AssetType(data[0..<1])) - XCTAssertNil(AssetType(data[0..<2])) - XCTAssertEqual(AssetType(data[0..<12]), .webp) - XCTAssertEqual(AssetType(data), .webp) + #expect(AssetType(data[0..<1]) == nil) + #expect(AssetType(data[0..<2]) == nil) + #expect(AssetType(data[0..<12]) == .webp) + #expect(AssetType(data) == .webp) } } -class ImagePropertiesTests: XCTestCase { +@Suite struct ImagePropertiesTests { // MARK: JPEG - - func testDetectBaselineJPEG() { + + @Test func detectBaselineJPEG() { let data = Test.data(name: "baseline", extension: "jpeg") - XCTAssertNil(ImageProperties.JPEG(data[0..<1])) - XCTAssertNil(ImageProperties.JPEG(data[0..<2])) - XCTAssertNil(ImageProperties.JPEG(data[0..<3])) - XCTAssertEqual(ImageProperties.JPEG(data)?.isProgressive, false) + #expect(ImageProperties.JPEG(data[0..<1]) == nil) + #expect(ImageProperties.JPEG(data[0..<2]) == nil) + #expect(ImageProperties.JPEG(data[0..<3]) == nil) + #expect(ImageProperties.JPEG(data)?.isProgressive == false) } - - func testDetectProgressiveJPEG() { + + @Test func detectProgressiveJPEG() { let data = Test.data(name: "progressive", extension: "jpeg") // Not enough data - XCTAssertNil(ImageProperties.JPEG(Data())) - XCTAssertNil(ImageProperties.JPEG(data[0..<2])) - + #expect(ImageProperties.JPEG(Data()) == nil) + #expect(ImageProperties.JPEG(data[0..<2]) == nil) + // Enough to determine image format - XCTAssertNil(ImageProperties.JPEG(data[0..<3])) - XCTAssertNil(ImageProperties.JPEG(data[0...30])) - + #expect(ImageProperties.JPEG(data[0..<3]) == nil) + #expect(ImageProperties.JPEG(data[0...30]) == nil) + // Just before the first scan - XCTAssertNil(ImageProperties.JPEG(data[0...358])) - XCTAssertEqual(ImageProperties.JPEG(data[0...359])?.isProgressive, true) - + #expect(ImageProperties.JPEG(data[0...358]) == nil) + #expect(ImageProperties.JPEG(data[0...359])?.isProgressive == true) + // Full image - XCTAssertEqual(ImageProperties.JPEG(data[0...359])?.isProgressive, true) + #expect(ImageProperties.JPEG(data[0...359])?.isProgressive == true) } } From f9438e31c059f64f074bde8a62ac1fd98d604219 Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 17 Nov 2024 14:12:30 -0500 Subject: [PATCH 72/73] Update ImageDecoderRegistryTests --- .../NukeTests/ImageDecoderRegistryTests.swift | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Tests/NukeTests/ImageDecoderRegistryTests.swift b/Tests/NukeTests/ImageDecoderRegistryTests.swift index b22864fec..c59889084 100644 --- a/Tests/NukeTests/ImageDecoderRegistryTests.swift +++ b/Tests/NukeTests/ImageDecoderRegistryTests.swift @@ -2,20 +2,20 @@ // // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). -import XCTest +import Testing @testable import Nuke -final class ImageDecoderRegistryTests: XCTestCase { - func testDefaultDecoderIsReturned() { +@Suite struct ImageDecoderRegistryTests { + @Test func defaultDecoderIsReturned() { // Given let context = ImageDecodingContext.mock // Then let decoder = ImageDecoderRegistry().decoder(for: context) - XCTAssertTrue(decoder is ImageDecoders.Default) + #expect(decoder is ImageDecoders.Default) } - func testRegisterDecoder() { + @Test func registerDecoder() { // Given let registry = ImageDecoderRegistry() let context = ImageDecodingContext.mock @@ -27,7 +27,7 @@ final class ImageDecoderRegistryTests: XCTestCase { // Then let decoder1 = registry.decoder(for: context) as? MockImageDecoder - XCTAssertEqual(decoder1?.name, "A") + #expect(decoder1?.name == "A") // When registry.register { _ in @@ -36,27 +36,27 @@ final class ImageDecoderRegistryTests: XCTestCase { // Then let decoder2 = registry.decoder(for: context) as? MockImageDecoder - XCTAssertEqual(decoder2?.name, "B") + #expect(decoder2?.name == "B") } - - func testClearDecoders() { + + @Test func clearDecoders() { // Given let registry = ImageDecoderRegistry() let context = ImageDecodingContext.mock - + registry.register { _ in return MockImageDecoder(name: "A") } // When registry.clear() - + // Then let noDecoder = registry.decoder(for: context) - XCTAssertNil(noDecoder) + #expect(noDecoder == nil) } - func testWhenReturningNextDecoderIsEvaluated() { + @Test func whenReturningNextDecoderIsEvaluated() { // Given let registry = ImageDecoderRegistry() registry.register { _ in @@ -68,6 +68,6 @@ final class ImageDecoderRegistryTests: XCTestCase { let decoder = ImageDecoderRegistry().decoder(for: context) // Then - XCTAssertTrue(decoder is ImageDecoders.Default) + #expect(decoder is ImageDecoders.Default) } } From 056d66f3f16d806cbb6ff85e503d9c09fc06669f Mon Sep 17 00:00:00 2001 From: kean Date: Sun, 17 Nov 2024 14:12:49 -0500 Subject: [PATCH 73/73] Update ImageEncoderTests --- Tests/NukeTests/ImageEncoderTests.swift | 84 ++++++++++++------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/Tests/NukeTests/ImageEncoderTests.swift b/Tests/NukeTests/ImageEncoderTests.swift index 18f0516d3..549dc3043 100644 --- a/Tests/NukeTests/ImageEncoderTests.swift +++ b/Tests/NukeTests/ImageEncoderTests.swift @@ -2,95 +2,95 @@ // // Copyright (c) 2015-2024 Alexander Grebenyuk (github.com/kean). -import XCTest +import Testing @testable import Nuke -final class ImageEncoderTests: XCTestCase { - func testEncodeImage() throws { +@Suite struct ImageEncoderTests { + @Test func encodeImage() throws { // Given let image = Test.image let encoder = ImageEncoders.Default() - + // When - let data = try XCTUnwrap(encoder.encode(image)) - + let data = try #require(encoder.encode(image)) + // Then - XCTAssertEqual(AssetType(data), .jpeg) + #expect(AssetType(data) == .jpeg) } - - func testEncodeImagePNGOpaque() throws { + + @Test func encodeImagePNGOpaque() throws { // Given let image = Test.image(named: "fixture", extension: "png") let encoder = ImageEncoders.Default() - + // When - let data = try XCTUnwrap(encoder.encode(image)) - + let data = try #require(encoder.encode(image)) + // Then #if os(macOS) // It seems that on macOS, NSImage created from png has an alpha // component regardless of whether the input image has it. - XCTAssertEqual(AssetType(data), .png) + #expect(AssetType(data) == .png) #else - XCTAssertEqual(AssetType(data), .jpeg) + #expect(AssetType(data) == .jpeg) #endif } - - func testEncodeImagePNGTransparent() throws { + + @Test func encodeImagePNGTransparent() throws { // Given let image = Test.image(named: "swift", extension: "png") let encoder = ImageEncoders.Default() - + // When - let data = try XCTUnwrap(encoder.encode(image)) - + let data = try #require(encoder.encode(image)) + // Then - XCTAssertEqual(AssetType(data), .png) + #expect(AssetType(data) == .png) } - - func testPrefersHEIF() throws { + + @Test func prefersHEIF() throws { // Given let image = Test.image var encoder = ImageEncoders.Default() encoder.isHEIFPreferred = true - + // When - let data = try XCTUnwrap(encoder.encode(image)) - + let data = try #require(encoder.encode(image)) + // Then - XCTAssertNil(AssetType(data)) // TODO: update when HEIF support is added + #expect(AssetType(data) == nil) // TODO: update when HEIF support is added // TODO: update when HEIF support is added } - + #if os(iOS) || os(tvOS) || os(visionOS) - - func testEncodeCoreImageBackedImage() throws { + + @Test func encodeCoreImageBackedImage() throws { // Given let image = try ImageProcessors.GaussianBlur().processThrowing(Test.image) let encoder = ImageEncoders.Default() - + // When - let data = try XCTUnwrap(encoder.encode(image)) - + let data = try #require(encoder.encode(image)) + // Then encoded as PNG because GaussianBlur produces // images with alpha channel - XCTAssertEqual(AssetType(data), .png) + #expect(AssetType(data) == .png) } - + #endif - + // MARK: - Misc - - func testIsOpaqueWithOpaquePNG() { + + @Test func isOpaqueWithOpaquePNG() { let image = Test.image(named: "fixture", extension: "png") #if os(macOS) - XCTAssertFalse(image.cgImage!.isOpaque) + #expect(!image.cgImage!.isOpaque) #else - XCTAssertTrue(image.cgImage!.isOpaque) + #expect(image.cgImage!.isOpaque) #endif } - - func testIsOpaqueWithTransparentPNG() { + + @Test func isOpaqueWithTransparentPNG() { let image = Test.image(named: "swift", extension: "png") - XCTAssertFalse(image.cgImage!.isOpaque) + #expect(!image.cgImage!.isOpaque) } }