1
1
mirror of https://github.com/kean/Nuke.git synced 2024-11-24 11:26:14 +03:00

Update ImagePrefetcher

This commit is contained in:
kean 2024-10-26 16:37:31 -04:00
parent 0e5273b1bf
commit 8ab81b7d00
6 changed files with 63 additions and 85 deletions

View File

@ -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 */,

View File

@ -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
}

View File

@ -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]))
}

View File

@ -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))
}

View File

@ -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")

View File

@ -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()
withSuspendedDataLoader(for: pipeline, expectedRequestCount: 1) {
expectPrefetcherToComplete()
// WHEN
prefetcher.startPrefetching(with: [Test.url])
prefetcher.startPrefetching(with: [Test.url])
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()
}
}