Ver Fonte

Add async versions to manager

onevcat há 2 anos atrás
pai
commit
92a523d56c

+ 107 - 0
Sources/General/KingfisherManager.swift

@@ -708,6 +708,113 @@ public class KingfisherManager {
     }
 }
 
+// Concurrency
+extension KingfisherManager {
+    /// Gets an image from a given resource.
+    /// - Parameters:
+    ///   - resource: The `Resource` object defines data information like key or URL.
+    ///   - options: Options to use when creating the image.
+    ///   - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
+    ///                    `expectedContentLength`, this block will not be called. `progressBlock` is always called in
+    ///                    main queue.
+    ///
+    /// - Note: This method will first check whether the requested `resource` is already in cache or not. If cached,
+    ///    it returns the value after the cached image retrieved. Otherwise, it
+    ///    will download the `resource`, store it in cache, then returns the value.
+    public func retrieveImage(
+        with resource: Resource,
+        options: KingfisherOptionsInfo? = nil,
+        progressBlock: DownloadProgressBlock? = nil
+    ) async throws -> RetrieveImageResult
+    {
+        try await retrieveImage(
+            with: resource.convertToSource(),
+            options: options,
+            progressBlock: progressBlock
+        )
+    }
+    
+    /// Gets an image from a given resource.
+    ///
+    /// - Parameters:
+    ///   - source: The `Source` object defines data information from network or a data provider.
+    ///   - options: Options to use when creating the image.
+    ///   - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an
+    ///                    `expectedContentLength`, this block will not be called. `progressBlock` is always called in
+    ///                    main queue.
+    ///
+    /// - Note: This method will first check whether the requested `resource` is already in cache or not. If cached,
+    ///    it returns the value after the cached image retrieved. Otherwise, it
+    ///    will download the `resource`, store it in cache, then returns the value.
+    ///
+    public func retrieveImage(
+        with source: Source,
+        options: KingfisherOptionsInfo? = nil,
+        progressBlock: DownloadProgressBlock? = nil
+    ) async throws -> RetrieveImageResult
+    {
+        let options = currentDefaultOptions + (options ?? .empty)
+        let info = KingfisherParsedOptionsInfo(options)
+        return try await retrieveImage(
+            with: source,
+            options: info,
+            progressBlock: progressBlock
+        )
+    }
+    
+    func retrieveImage(
+        with source: Source,
+        options: KingfisherParsedOptionsInfo,
+        progressBlock: DownloadProgressBlock? = nil
+    ) async throws -> RetrieveImageResult
+    {
+        var info = options
+        if let block = progressBlock {
+            info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
+        }
+        return try await retrieveImage(
+            with: source,
+            options: info,
+            progressiveImageSetter: nil
+        )
+    }
+    
+    func retrieveImage(
+        with source: Source,
+        options: KingfisherParsedOptionsInfo,
+        progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil,
+        referenceTaskIdentifierChecker: (() -> Bool)? = nil
+    ) async throws -> RetrieveImageResult
+    {
+        let task = CancellationDownloadTask()
+        return try await withTaskCancellationHandler {
+            try await withCheckedThrowingContinuation { continuation in
+                let downloadTask = retrieveImage(
+                    with: source,
+                    options: options,
+                    downloadTaskUpdated: { newTask in
+                        Task {
+                            await task.setTask(newTask)
+                        }
+                    },
+                    progressiveImageSetter: progressiveImageSetter,
+                    referenceTaskIdentifierChecker: referenceTaskIdentifierChecker,
+                    completionHandler: { result in
+                        continuation.resume(with: result)
+                    }
+                )
+                Task {
+                    await task.setTask(downloadTask)
+                }
+            }
+        } onCancel: {
+            Task {
+                await task.task?.cancel()
+            }
+        }
+    }
+}
+
 class RetrievingContext {
 
     var options: KingfisherParsedOptionsInfo

+ 7 - 0
Sources/Networking/ImageDownloader.swift

@@ -87,6 +87,13 @@ public struct DownloadTask {
     }
 }
 
+actor CancellationDownloadTask {
+    var task: DownloadTask?
+    func setTask(_ task: DownloadTask?) {
+        self.task = task
+    }
+}
+
 extension DownloadTask {
     enum WrappedTask {
         case download(DownloadTask)

+ 73 - 0
Tests/KingfisherTests/KingfisherManagerTests.swift

@@ -89,6 +89,32 @@ class KingfisherManagerTests: XCTestCase {
         waitForExpectations(timeout: 3, handler: nil)
     }
     
+    func testRetrieveImageAsync() async throws {
+        let url = testURLs[0]
+        stub(url, data: testImageData)
+
+        let manager = self.manager!
+        
+        var result = try await manager.retrieveImage(with: url)
+        XCTAssertNotNil(result.image)
+        XCTAssertEqual(result.cacheType, .none)
+        
+        result = try await manager.retrieveImage(with: url)
+        XCTAssertNotNil(result.image)
+        XCTAssertEqual(result.cacheType, .memory)
+        
+        manager.cache.clearMemoryCache()
+        result = try await manager.retrieveImage(with: url)
+        XCTAssertNotNil(result.image)
+        XCTAssertEqual(result.cacheType, .disk)
+        
+        manager.cache.clearMemoryCache()
+        await manager.cache.clearDiskCache()
+        result = try await manager.retrieveImage(with: url)
+        XCTAssertNotNil(result.image)
+        XCTAssertEqual(result.cacheType, .none)
+    }
+    
     func testRetrieveImageWithProcessor() {
         let exp = expectation(description: #function)
         let url = testURLs[0]
@@ -149,6 +175,53 @@ class KingfisherManagerTests: XCTestCase {
         waitForExpectations(timeout: 3, handler: nil)
     }
     
+    func testRetrieveImageCancel() {
+        let exp = expectation(description: #function)
+        let url = testURLs[0]
+        let stub = delayedStub(url, data: testImageData, length: 123)
+
+        let task = manager.retrieveImage(with: url) {
+            result in
+            XCTAssertNotNil(result.error)
+            XCTAssertTrue(result.error!.isTaskCancelled)
+            exp.fulfill()
+        }
+
+        XCTAssertNotNil(task)
+        task?.cancel()
+        _ = stub.go()
+        waitForExpectations(timeout: 3, handler: nil)
+    }
+    
+    actor CallingChecker {
+        var called = false
+        func mark() {
+            called = true
+        }
+    }
+    
+    func testRetrieveImageCancelAsync() async throws {
+        let url = testURLs[0]
+        let stub = delayedStub(url, data: testImageData, length: 123)
+
+        let checker = CallingChecker()
+        let task = Task {
+            do {
+                _ = try await manager.retrieveImage(with: url)
+                XCTFail()
+            } catch {
+                await checker.mark()
+                XCTAssertTrue((error as! KingfisherError).isTaskCancelled)
+            }
+        }
+        try await Task.sleep(nanoseconds: NSEC_PER_SEC / 10)
+        task.cancel()
+         _ = stub.go()
+        try await Task.sleep(nanoseconds: NSEC_PER_SEC / 10)
+        let catchCalled = await checker.called
+        XCTAssertTrue(catchCalled)
+    }
+    
     func testSuccessCompletionHandlerRunningOnMainQueueDefaultly() {
         let progressExpectation = expectation(description: "progressBlock running on main queue")
         let completionExpectation = expectation(description: "completionHandler running on main queue")