Skip to content

Commit

Permalink
Add async versions to manager
Browse files Browse the repository at this point in the history
  • Loading branch information
onevcat committed Sep 9, 2023
1 parent 411ef3f commit 0e17d3e
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 0 deletions.
107 changes: 107 additions & 0 deletions Sources/General/KingfisherManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions Sources/Networking/ImageDownloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
73 changes: 73 additions & 0 deletions Tests/KingfisherTests/KingfisherManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit 0e17d3e

Please sign in to comment.