From fdf1e5c7bb15f18dc0d3546df5505eec56455060 Mon Sep 17 00:00:00 2001 From: Eneko Alonso Date: Thu, 1 Jul 2021 12:33:03 -0700 Subject: [PATCH] Allow passing a custom completion queue to ImageDownloader (#162) ### Summary Allow passing a custom operation queue to `ImageDownloader` to be used when calling the completion callback. ### Implementation - Add `completionQueue` parameter to initializer (defaults to `nil`) - If no value is passed, `ImageDownloader` will behave as before, defaulting to the current operation queue for completion, or main if there is no current queue. - When a queue is specified, it will be used instead. --- .../Networking/Images/ImageDownloader.swift | 24 ++++++----- .../Images/ImageDownloaderTests.swift | 40 +++++++++++++++++++ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/Sources/Conduit/Networking/Images/ImageDownloader.swift b/Sources/Conduit/Networking/Images/ImageDownloader.swift index 16f0bf5..6cf78db 100644 --- a/Sources/Conduit/Networking/Images/ImageDownloader.swift +++ b/Sources/Conduit/Networking/Images/ImageDownloader.swift @@ -66,17 +66,22 @@ public final class ImageDownloader: ImageDownloaderType { private let sessionClient: URLSessionClientType private var sessionProxyMap: [String: SessionTaskProxyType] = [:] private var completionHandlerMap: [String: [CompletionHandler]] = [:] + private let completionQueue: OperationQueue? private let serialQueue = DispatchQueue( label: "com.mindbodyonline.Conduit.ImageDownloader-\(UUID().uuidString)" ) /// Initializes a new ImageDownloader /// - Parameters: - /// - cache: The image cache in which to store downloaded images - /// - sessionClient: The URLSessionClient to be used to download images - public init(cache: URLImageCache, sessionClient: URLSessionClientType = URLSessionClient()) { + /// - cache: The image cache in which to store downloaded images + /// - sessionClient: The URLSessionClient to be used to download images + /// - completionQueue: An optional operation queue for completion callback + public init(cache: URLImageCache, + sessionClient: URLSessionClientType = URLSessionClient(), + completionQueue: OperationQueue? = nil) { self.cache = cache self.sessionClient = sessionClient + self.completionQueue = completionQueue } /// Downloads an image or retrieves it from the cache if previously downloaded. @@ -116,6 +121,7 @@ public final class ImageDownloader: ImageDownloaderType { // Strongly capture self within the completion handler to ensure // ImageDownloader is persisted long enough to respond + let strongSelf = self proxy = self.sessionClient.begin(request: request) { data, response, error in var image: Image? if let data = data { @@ -123,11 +129,11 @@ public final class ImageDownloader: ImageDownloaderType { } if let image = image { - self.cache.cache(image: image, for: request) + strongSelf.cache.cache(image: image, for: request) } let response = Response(image: image, error: error, urlResponse: response, isFromCache: false) - let queue = OperationQueue.current ?? OperationQueue.main + let queue = strongSelf.completionQueue ?? .current ?? .main func execute(handler: @escaping CompletionHandler) { queue.addOperation { @@ -136,10 +142,10 @@ public final class ImageDownloader: ImageDownloaderType { } // Intentional retain cycle that releases immediately after execution - self.serialQueue.async { - self.sessionProxyMap[cacheIdentifier] = nil - self.completionHandlerMap[cacheIdentifier]?.forEach(execute) - self.completionHandlerMap[cacheIdentifier] = nil + strongSelf.serialQueue.async { + strongSelf.sessionProxyMap[cacheIdentifier] = nil + strongSelf.completionHandlerMap[cacheIdentifier]?.forEach(execute) + strongSelf.completionHandlerMap[cacheIdentifier] = nil } } diff --git a/Tests/ConduitTests/Networking/Images/ImageDownloaderTests.swift b/Tests/ConduitTests/Networking/Images/ImageDownloaderTests.swift index 572b2d2..493c6c8 100644 --- a/Tests/ConduitTests/Networking/Images/ImageDownloaderTests.swift +++ b/Tests/ConduitTests/Networking/Images/ImageDownloaderTests.swift @@ -114,4 +114,44 @@ class ImageDownloaderTests: XCTestCase { waitForExpectations(timeout: 5) } + func testMainOperationQueue() throws { + // GIVEN a main operation queue + let expectedQueue = OperationQueue.main + + // AND a configured Image Downloader instance + let imageDownloadedExpectation = expectation(description: "image downloaded") + let sut = ImageDownloader(cache: AutoPurgingURLImageCache()) + let url = try URL(absoluteString: "https://httpbin.org/image/jpeg") + let imageRequest = URLRequest(url: url) + + // WHEN downloading an image + sut.downloadImage(for: imageRequest) { _ in + // THEN the completion handler is called in the expected queue + XCTAssertEqual(OperationQueue.current, expectedQueue) + imageDownloadedExpectation.fulfill() + } + + waitForExpectations(timeout: 5) + } + + func testCustomOperationQueue() throws { + // GIVEN a custom operation queue + let customQueue = OperationQueue() + + // AND a configured Image Downloader instance with our custom completion queue + let imageDownloadedExpectation = expectation(description: "image downloaded") + let sut = ImageDownloader(cache: AutoPurgingURLImageCache(), completionQueue: customQueue) + let url = try URL(absoluteString: "https://httpbin.org/image/jpeg") + let imageRequest = URLRequest(url: url) + + // WHEN downloading an image + sut.downloadImage(for: imageRequest) { _ in + // THEN the completion handler is called in our custom queue + XCTAssertEqual(OperationQueue.current, customQueue) + imageDownloadedExpectation.fulfill() + } + + waitForExpectations(timeout: 5) + } + }