Răsfoiți Sursa

Add an option to specify processing queue

onevcat 7 ani în urmă
părinte
comite
88ed8ef104

+ 6 - 0
Sources/General/Kingfisher.swift

@@ -47,6 +47,8 @@ import WatchKit
 #endif
 #endif
 
+/// Wrapper for Kingfisher compatible types. This type provides an extension point for
+/// connivence methods in Kingfisher.
 public struct KingfisherWrapper<Base> {
     public let base: Base
     public init(_ base: Base) {
@@ -54,9 +56,13 @@ public struct KingfisherWrapper<Base> {
     }
 }
 
+/// Represents a type which is compatible with Kingfisher. You can use `kf` property to get a
+/// value in the namespace of Kingfisher.
 public protocol KingfisherCompatible { }
 
 public extension KingfisherCompatible {
+    
+    /// Gets a namespace holder for Kingfisher compatible types.
     public var kf: KingfisherWrapper<Self> {
         get { return KingfisherWrapper(self) }
         set { }

+ 10 - 0
Sources/General/KingfisherError.swift

@@ -166,7 +166,13 @@ public enum KingfisherError: Error {
         /// The resource task is finished, but it is not the one expected now. This usually happens when you set another
         /// resource on the view without cancelling the current on-going one. The previous setting task will fail with
         /// this `.notCurrentSourceTask` error when a result got, regardless of it being successful or not for that task.
+        /// The result of this original task is contained in the associated value.
         /// Code 5002.
+        /// - result: The `RetrieveImageResult` if the source task is finished without problem. `nil` if an error
+        ///           happens.
+        /// - error: The `Error` if an issue happens during image setting task. `nil` if the task finishes without
+        ///          problem.
+        /// - source: The original source value of the taks.
         case notCurrentSourceTask(result: RetrieveImageResult?, error: Error?, source: Source)
 
         /// An error happens during getting data from an `ImageDataProvider`. Code 5003.
@@ -218,6 +224,8 @@ public enum KingfisherError: Error {
 }
 
 extension KingfisherError: LocalizedError {
+    
+    /// A localized message describing what error occurred.
     public var errorDescription: String? {
         switch self {
         case .requestError(let reason): return reason.errorDescription
@@ -230,6 +238,8 @@ extension KingfisherError: LocalizedError {
 }
 
 extension KingfisherError: CustomNSError {
+
+    /// The error code within the given domain.
     public var errorCode: Int {
         switch self {
         case .requestError(let reason): return reason.errorCode

+ 26 - 15
Sources/General/KingfisherManager.swift

@@ -78,7 +78,7 @@ public class KingfisherManager {
         return [.downloader(downloader), .targetCache(cache)] + defaultOptions
     }
 
-    private let processQueue: DispatchQueue
+    private let processingQueue: CallbackQueue
     
     private convenience init() {
         self.init(downloader: .default, cache: .default)
@@ -89,7 +89,7 @@ public class KingfisherManager {
         self.cache = cache
 
         let processQueueName = "com.onevcat.Kingfisher.KingfisherManager.processQueue.\(UUID().uuidString)"
-        processQueue = DispatchQueue(label: processQueueName)
+        processingQueue = .dispatch(DispatchQueue(label: processQueueName))
     }
 
     /// Gets an image from a given resource.
@@ -193,9 +193,23 @@ public class KingfisherManager {
         provider.data { result in
             switch result {
             case .success(let data):
-                let image = options.processor.process(item: .data(data), options: options)!
-                let result = ImageLoadingResult(image: image, url: nil, originalData: data)
-                completionHandler?(.success(result))
+                (options.processingQueue ?? self.processingQueue).execute {
+                    let processor = options.processor
+                    let processingItem = ImageProcessItem.data(data)
+                    guard let image = processor.process(item: processingItem, options: options) else {
+                        options.callbackQueue.execute {
+                            completionHandler?(
+                                .failure(
+                                    .processorError(reason: .processingFailed(processor: processor, item: processingItem))
+                                )
+                            )
+                        }
+                        return
+                    }
+                    
+                    let result = ImageLoadingResult(image: image, url: nil, originalData: data)
+                    options.callbackQueue.execute { completionHandler?(.success(result)) }
+                }
             case .failure(let error):
                 options.callbackQueue.execute {
                     completionHandler?(
@@ -238,14 +252,12 @@ public class KingfisherManager {
                 let needToCacheOriginalImage = options.cacheOriginalImage &&
                     options.processor != DefaultImageProcessor.default
                 if needToCacheOriginalImage {
-                    self.processQueue.async {
-                        let originalCache = options.originalCache ?? targetCache
-                        originalCache.storeToDisk(
-                            value.originalData,
-                            forKey: source.cacheKey,
-                            processorIdentifier: DefaultImageProcessor.default.identifier,
-                            expiration: options.diskCacheExpiration)
-                    }
+                    let originalCache = options.originalCache ?? targetCache
+                    originalCache.storeToDisk(
+                        value.originalData,
+                        forKey: source.cacheKey,
+                        processorIdentifier: DefaultImageProcessor.default.identifier,
+                        expiration: options.diskCacheExpiration)
                 }
 
                 if !options.waitForCache {
@@ -335,8 +347,7 @@ public class KingfisherManager {
             originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { result in
                 if let image = result.value?.image {
                     let processor = options.processor
-                    let processQueue = self.processQueue
-                    processQueue.async {
+                    (options.processingQueue ?? self.processingQueue).execute {
                         let item = ImageProcessItem.image(image)
                         guard let processedImage = processor.process(item: item, options: options) else {
                             let error = KingfisherError.processorError(

+ 8 - 0
Sources/General/KingfisherOptionsInfo.swift

@@ -196,6 +196,12 @@ public enum KingfisherOptionsInfoItem {
     /// expiration in its config for all items. If set, the `DiskStorage.Backend` will use this associated
     /// value to overwrite the config setting for this caching item.
     case diskCacheExpiration(StorageExpiration)
+    
+    /// Decides on which queue the image processing should happen. By default, Kingfisher uses a pre-defined serial
+    /// queue to process images. Use this option to change this behavior. For example, specify a `.mainCurrentOrAsync`
+    /// to let the image be processed in main queue to prevent a possible flickering (but with a possibility of
+    /// blocking the UI, especially if the processor needs a lot of time to run).
+    case processingQueue(CallbackQueue)
 }
 
 // Improve performance by parsing the input `KingfisherOptionsInfo` (self) first.
@@ -232,6 +238,7 @@ public struct KingfisherParsedOptionsInfo {
     public var loadDiskFileSynchronously = false
     public var memoryCacheExpiration: StorageExpiration? = nil
     public var diskCacheExpiration: StorageExpiration? = nil
+    public var processingQueue: CallbackQueue? = nil
 
     public init(_ info: KingfisherOptionsInfo?) {
         guard let info = info else { return }
@@ -265,6 +272,7 @@ public struct KingfisherParsedOptionsInfo {
             case .callbackDispatchQueue(let value): callbackQueue = value.map { .dispatch($0) } ?? .mainCurrentOrAsync
             case .memoryCacheExpiration(let expiration): memoryCacheExpiration = expiration
             case .diskCacheExpiration(let expiration): diskCacheExpiration = expiration
+            case .processingQueue(let queue): processingQueue = queue
             }
         }
 

+ 6 - 3
Sources/Networking/ImageDataProcessor.swift

@@ -26,24 +26,27 @@
 
 import Foundation
 
-let processQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process")
+private let sharedProcessingQueue: CallbackQueue =
+    .dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process"))
 
 // Handles image processing work on an own process queue.
 class ImageDataProcessor {
     let data: Data
     let callbacks: [SessionDataTask.TaskCallback]
+    let queue: CallbackQueue
 
     // Note: We have an optimization choice there, to reduce queue dispatch by checking callback
     // queue settings in each option...
     let onImageProcessed = Delegate<(Result<Image, KingfisherError>, SessionDataTask.TaskCallback), Void>()
 
-    init(data: Data, callbacks: [SessionDataTask.TaskCallback]) {
+    init(data: Data, callbacks: [SessionDataTask.TaskCallback], processingQueue: CallbackQueue?) {
         self.data = data
         self.callbacks = callbacks
+        self.queue = processingQueue ?? sharedProcessingQueue
     }
 
     func process() {
-        processQueue.async(execute: doProcess)
+        queue.execute(doProcess)
     }
 
     private func doProcess() {

+ 1 - 1
Sources/Networking/ImageDownloader.swift

@@ -240,7 +240,7 @@ open class ImageDownloader {
             switch result {
             // Download finished. Now process the data to an image.
             case .success(let (data, response)):
-                let processor = ImageDataProcessor(data: data, callbacks: callbacks)
+                let processor = ImageDataProcessor(data: data, callbacks: callbacks, processingQueue: options.processingQueue)
                 processor.onImageProcessed.delegate(on: self) { (self, result) in
                     // `onImageProcessed` will be called for `callbacks.count` times, with each
                     // `SessionDataTask.TaskCallback` as the input parameter.

+ 19 - 1
Tests/KingfisherTests/KingfisherManagerTests.swift

@@ -361,6 +361,23 @@ class KingfisherManagerTests: XCTestCase {
         waitForExpectations(timeout: 1, handler: nil)
     }
     
+    func testFailingProcessOnDataProviderImage() {
+        let provider = SimpleImageDataProvider { .success(testImageData) }
+        var called = false
+        let p = FailingProcessor()
+        let options = [KingfisherOptionsInfoItem.processor(p), .processingQueue(.mainCurrentOrAsync)]
+        _ = manager.retrieveImage(with: .provider(provider), options: options) { result in
+            called = true
+            XCTAssertNotNil(result.error)
+            if case .processorError(reason: .processingFailed(let processor, _)) = result.error! {
+                XCTAssertEqual(processor.identifier, p.identifier)
+            } else {
+                XCTFail()
+            }
+        }
+        XCTAssertTrue(called)
+    }
+    
     func testCacheOriginalImageWithOriginalCache() {
         let exp = expectation(description: #function)
         let url = testURLs[0]
@@ -633,7 +650,8 @@ class KingfisherManagerTests: XCTestCase {
     func testRetrieveWithImageProvider() {
         let provider = SimpleImageDataProvider { .success(testImageData) }
         var called = false
-        _ = manager.retrieveImage(with: .provider(provider)) { result in
+        _ = manager.retrieveImage(with: .provider(provider), options: [.processingQueue(.mainCurrentOrAsync)]) {
+            result in
             called = true
             XCTAssertNotNil(result.value)
             XCTAssertTrue(result.value!.image.renderEqual(to: testImage))