diff --git a/Sources/ImageCache.swift b/Sources/ImageCache.swift index 860a569c5..e45376863 100644 --- a/Sources/ImageCache.swift +++ b/Sources/ImageCache.swift @@ -276,45 +276,47 @@ extension ImageCache { } var block: RetrieveImageDiskTask? + let options = options ?? KingfisherEmptyOptionsInfo + if let image = self.retrieveImageInMemoryCacheForKey(key) { - //Found image in memory cache. - if let options = options where options.backgroundDecode { + if options.backgroundDecode { dispatch_async(self.processQueue, { () -> Void in let result = image.kf_decodedImage(scale: options.scaleFactor) - dispatch_async(options.callbackDispatchQueue, { () -> Void in + dispatch_async_safely_to_queue(options.callbackDispatchQueue, { () -> Void in completionHandler(result, .Memory) }) }) } else { - completionHandler(image, .Memory) + dispatch_async_safely_to_queue(options.callbackDispatchQueue, { () -> Void in + completionHandler(image, .Memory) + }) } } else { var sSelf: ImageCache! = self block = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS) { // Begin to load image from disk - let options = options ?? KingfisherEmptyOptionsInfo if let image = sSelf.retrieveImageInDiskCacheForKey(key, scale: options.scaleFactor) { if options.backgroundDecode { dispatch_async(sSelf.processQueue, { () -> Void in let result = image.kf_decodedImage(scale: options.scaleFactor) sSelf.storeImage(result!, forKey: key, toDisk: false, completionHandler: nil) - dispatch_async(options.callbackDispatchQueue, { () -> Void in + dispatch_async_safely_to_queue(options.callbackDispatchQueue, { () -> Void in completionHandler(result, .Memory) sSelf = nil }) }) } else { sSelf.storeImage(image, forKey: key, toDisk: false, completionHandler: nil) - dispatch_async(options.callbackDispatchQueue, { () -> Void in + dispatch_async_safely_to_queue(options.callbackDispatchQueue, { () -> Void in completionHandler(image, .Disk) sSelf = nil }) } } else { // No image found from either memory or disk - dispatch_async(options.callbackDispatchQueue, { () -> Void in + dispatch_async_safely_to_queue(options.callbackDispatchQueue, { () -> Void in completionHandler(nil, nil) sSelf = nil }) diff --git a/Sources/ImageDownloader.swift b/Sources/ImageDownloader.swift index ff63e7a70..72136a7d0 100644 --- a/Sources/ImageDownloader.swift +++ b/Sources/ImageDownloader.swift @@ -318,7 +318,9 @@ extension ImageDownloader: NSURLSessionDataDelegate { fetchLoad.responseData.appendData(data) for callbackPair in fetchLoad.callbacks { - callbackPair.progressBlock?(receivedSize: Int64(fetchLoad.responseData.length), totalSize: dataTask.response!.expectedContentLength) + dispatch_async(dispatch_get_main_queue(), { () -> Void in + callbackPair.progressBlock?(receivedSize: Int64(fetchLoad.responseData.length), totalSize: dataTask.response!.expectedContentLength) + }) } } } @@ -355,11 +357,14 @@ extension ImageDownloader: NSURLSessionDataDelegate { private func callbackWithImage(image: Image?, error: NSError?, imageURL: NSURL, originalData: NSData?) { if let callbackPairs = fetchLoadForKey(imageURL)?.callbacks { + let options = fetchLoadForKey(imageURL)?.options ?? KingfisherEmptyOptionsInfo self.cleanForURL(imageURL) for callbackPair in callbackPairs { - callbackPair.completionHander?(image: image, error: error, imageURL: imageURL, originalData: originalData) + dispatch_async_safely_to_queue(options.callbackDispatchQueue, { () -> Void in + callbackPair.completionHander?(image: image, error: error, imageURL: imageURL, originalData: originalData) + }) } } } diff --git a/Sources/ImageView+Kingfisher.swift b/Sources/ImageView+Kingfisher.swift index e153fda5b..3bb48c235 100644 --- a/Sources/ImageView+Kingfisher.swift +++ b/Sources/ImageView+Kingfisher.swift @@ -140,6 +140,9 @@ extension ImageView { - parameter completionHandler: Called when the image retrieved and set. - returns: A task represents the retrieving process. + + - note: `completionHandler` will be invoked in main thread. + The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. */ public func kf_setImageWithResource(resource: Resource, placeholderImage: Image?, @@ -158,6 +161,9 @@ extension ImageView { - parameter completionHandler: Called when the image retrieved and set. - returns: A task represents the retrieving process. + + - note: `completionHandler` will be invoked in main thread. + The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. */ public func kf_setImageWithURL(URL: NSURL, placeholderImage: Image?, @@ -177,6 +183,9 @@ extension ImageView { - parameter completionHandler: Called when the image retrieved and set. - returns: A task represents the retrieving process. + + - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread. + The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. */ public func kf_setImageWithResource(resource: Resource, placeholderImage: Image?, @@ -199,16 +208,12 @@ extension ImageView { let task = KingfisherManager.sharedManager.retrieveImageWithResource(resource, optionsInfo: optionsInfo, progressBlock: { receivedSize, totalSize in if let progressBlock = progressBlock { - dispatch_async(dispatch_get_main_queue(), { () -> Void in - progressBlock(receivedSize: receivedSize, totalSize: totalSize) - - }) + progressBlock(receivedSize: receivedSize, totalSize: totalSize) } }, completionHandler: {[weak self] image, error, cacheType, imageURL in - dispatch_async_safely_main_queue { - + dispatch_async_safely_to_main_queue { guard let sSelf = self where imageURL == sSelf.kf_webURL else { completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL) return @@ -221,27 +226,26 @@ extension ImageView { completionHandler?(image: nil, error: error, cacheType: cacheType, imageURL: imageURL) return } - - + if let transitionItem = optionsInfo?.kf_firstMatchIgnoringAssociatedValue(.Transition(.None)), case .Transition(let transition) = transitionItem where cacheType == .None { -#if !os(OSX) - UIView.transitionWithView(sSelf, duration: 0.0, options: [], - animations: { - indicator?.kf_stopAnimating() - }, - completion: { finished in - UIView.transitionWithView(sSelf, duration: transition.duration, - options: transition.animationOptions, - animations: { - transition.animations?(sSelf, image) - }, - completion: { finished in - transition.completion?(finished) - completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL) + #if !os(OSX) + UIView.transitionWithView(sSelf, duration: 0.0, options: [], + animations: { + indicator?.kf_stopAnimating() + }, + completion: { finished in + UIView.transitionWithView(sSelf, duration: transition.duration, + options: transition.animationOptions, + animations: { + transition.animations?(sSelf, image) + }, + completion: { finished in + transition.completion?(finished) + completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL) }) }) -#endif + #endif } else { indicator?.kf_stopAnimating() sSelf.image = image @@ -265,6 +269,9 @@ extension ImageView { - parameter completionHandler: Called when the image retrieved and set. - returns: A task represents the retrieving process. + + - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread. + The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. */ public func kf_setImageWithURL(URL: NSURL, diff --git a/Sources/ThreadHelper.swift b/Sources/ThreadHelper.swift index d1f0ec586..1645eced0 100644 --- a/Sources/ThreadHelper.swift +++ b/Sources/ThreadHelper.swift @@ -26,11 +26,18 @@ import Foundation -func dispatch_async_safely_main_queue(block: ()->()) { - if NSThread.isMainThread() { +func dispatch_async_safely_to_main_queue(block: ()->()) { + dispatch_async_safely_to_queue(dispatch_get_main_queue(), block) +} + +// This methd will dispatch the `block` to a specified `queue`. +// If the `queue` is the main queue, and current thread is main thread, the block +// will be invoked immediately instead of being dispatched. +func dispatch_async_safely_to_queue(queue: dispatch_queue_t, _ block: ()->()) { + if queue === dispatch_get_main_queue() && NSThread.isMainThread() { block() } else { - dispatch_async(dispatch_get_main_queue()) { + dispatch_async(queue) { block() } } diff --git a/Sources/UIButton+Kingfisher.swift b/Sources/UIButton+Kingfisher.swift index 839d4614b..71a4750c3 100644 --- a/Sources/UIButton+Kingfisher.swift +++ b/Sources/UIButton+Kingfisher.swift @@ -145,6 +145,9 @@ extension UIButton { - parameter completionHandler: Called when the image retrieved and set. - returns: A task represents the retrieving process. + + - note: `completionHandler` will be invoked in main thread. + The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. */ public func kf_setImageWithResource(resource: Resource, forState state: UIControlState, @@ -165,6 +168,9 @@ extension UIButton { - parameter completionHandler: Called when the image retrieved and set. - returns: A task represents the retrieving process. + + - note: `completionHandler` will be invoked in main thread. + The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. */ public func kf_setImageWithURL(URL: NSURL, forState state: UIControlState, @@ -187,6 +193,9 @@ extension UIButton { - parameter completionHandler: Called when the image retrieved and set. - returns: A task represents the retrieving process. + + - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread. + The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. */ public func kf_setImageWithResource(resource: Resource, forState state: UIControlState, @@ -200,14 +209,11 @@ extension UIButton { let task = KingfisherManager.sharedManager.retrieveImageWithResource(resource, optionsInfo: optionsInfo, progressBlock: { receivedSize, totalSize in if let progressBlock = progressBlock { - dispatch_async(dispatch_get_main_queue(), { () -> Void in - progressBlock(receivedSize: receivedSize, totalSize: totalSize) - }) + progressBlock(receivedSize: receivedSize, totalSize: totalSize) } }, completionHandler: {[weak self] image, error, cacheType, imageURL in - - dispatch_async_safely_main_queue { + dispatch_async_safely_to_main_queue { if let sSelf = self { sSelf.kf_setImageTask(nil) @@ -235,6 +241,9 @@ extension UIButton { - parameter completionHandler: Called when the image retrieved and set. - returns: A task represents the retrieving process. + + - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread. + The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. */ public func kf_setImageWithURL(URL: NSURL, forState state: UIControlState, @@ -410,6 +419,9 @@ extension UIButton { - parameter completionHandler: Called when the image retrieved and set. - returns: A task represents the retrieving process. + + - note: `completionHandler` will be invoked in main thread. + The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. */ public func kf_setBackgroundImageWithResource(resource: Resource, forState state: UIControlState, @@ -430,6 +442,9 @@ extension UIButton { - parameter completionHandler: Called when the image retrieved and set. - returns: A task represents the retrieving process. + + - note: `completionHandler` will be invoked in main thread. + The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. */ public func kf_setBackgroundImageWithURL(URL: NSURL, forState state: UIControlState, @@ -452,6 +467,9 @@ extension UIButton { - parameter completionHandler: Called when the image retrieved and set. - returns: A task represents the retrieving process. + + - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread. + The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. */ public func kf_setBackgroundImageWithResource(resource: Resource, forState state: UIControlState, @@ -465,14 +483,11 @@ extension UIButton { let task = KingfisherManager.sharedManager.retrieveImageWithResource(resource, optionsInfo: optionsInfo, progressBlock: { receivedSize, totalSize in if let progressBlock = progressBlock { - dispatch_async(dispatch_get_main_queue(), { () -> Void in - progressBlock(receivedSize: receivedSize, totalSize: totalSize) - }) + progressBlock(receivedSize: receivedSize, totalSize: totalSize) } }, completionHandler: { [weak self] image, error, cacheType, imageURL in - dispatch_async_safely_main_queue { - + dispatch_async_safely_to_main_queue { if let sSelf = self { sSelf.kf_setBackgroundImageTask(nil) @@ -501,6 +516,9 @@ extension UIButton { - parameter completionHandler: Called when the image retrieved and set. - returns: A task represents the retrieving process. + + - note: Both the `progressBlock` and `completionHandler` will be invoked in main thread. + The `CallbackDispatchQueue` specified in `optionsInfo` will not be used in callbacks of this method. */ public func kf_setBackgroundImageWithURL(URL: NSURL, forState state: UIControlState, diff --git a/Tests/KingfisherTests/ImageViewExtensionTests.swift b/Tests/KingfisherTests/ImageViewExtensionTests.swift index 262c6afe2..f57e502a1 100644 --- a/Tests/KingfisherTests/ImageViewExtensionTests.swift +++ b/Tests/KingfisherTests/ImageViewExtensionTests.swift @@ -70,6 +70,7 @@ class ImageViewExtensionTests: XCTestCase { imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in progressBlockIsCalled = true + XCTAssertTrue(NSThread.isMainThread()) }) { (image, error, cacheType, imageURL) -> () in expectation.fulfill() @@ -80,6 +81,25 @@ class ImageViewExtensionTests: XCTestCase { XCTAssert(self.imageView.kf_webURL == imageURL, "Web URL should equal to the downloaded url.") XCTAssert(cacheType == .None, "The cache type should be none here. This image was just downloaded.") + XCTAssertTrue(NSThread.isMainThread()) + } + + waitForExpectationsWithTimeout(5, handler: nil) + } + + func testImageDownloadCompletionHandlerRunningOnMainQueue() { + let expectation = expectationWithDescription("wait for downloading image") + + let URLString = testKeys[0] + stubRequest("GET", URLString).andReturn(200).withBody(testImageData) + let URL = NSURL(string: URLString)! + + let customQueue = dispatch_queue_create("com.kingfisher.testQueue", DISPATCH_QUEUE_SERIAL) + imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: [.CallbackDispatchQueue(customQueue)], progressBlock: { (receivedSize, totalSize) -> () in + XCTAssertTrue(NSThread.isMainThread()) + }) { (image, error, cacheType, imageURL) -> () in + XCTAssertTrue(NSThread.isMainThread(), "The image extension callback should be always in main queue.") + expectation.fulfill() } waitForExpectationsWithTimeout(5, handler: nil) diff --git a/Tests/KingfisherTests/KingfisherManagerTests.swift b/Tests/KingfisherTests/KingfisherManagerTests.swift index 8cd943a5e..dd604c95a 100644 --- a/Tests/KingfisherTests/KingfisherManagerTests.swift +++ b/Tests/KingfisherTests/KingfisherManagerTests.swift @@ -129,4 +129,78 @@ class KingfisherManagerTests: XCTestCase { waitForExpectationsWithTimeout(5, handler: nil) } + + func testSuccessCompletionHandlerRunningOnMainQueueDefaultly() { + let progressExpectation = expectationWithDescription("progressBlock running on main queue") + let completionExpectation = expectationWithDescription("completionHandler running on main queue") + let URLString = testKeys[0] + stubRequest("GET", URLString).andReturn(200).withBody(testImageData) + + let URL = NSURL(string: URLString)! + + manager.retrieveImageWithURL(URL, optionsInfo: nil, progressBlock: { _, _ in + XCTAssertTrue(NSThread.isMainThread()) + progressExpectation.fulfill() + }, completionHandler: { _, error, _, _ in + XCTAssertNil(error) + XCTAssertTrue(NSThread.isMainThread()) + completionExpectation.fulfill() + }) + waitForExpectationsWithTimeout(5, handler: nil) + } + + func testErrorCompletionHandlerRunningOnMainQueueDefaultly() { + let expectation = expectationWithDescription("running on main queue") + let URLString = testKeys[0] + stubRequest("GET", URLString).andReturn(404) + + let URL = NSURL(string: URLString)! + + manager.retrieveImageWithURL(URL, optionsInfo: nil, progressBlock: { _, _ in + //won't be called + }, completionHandler: { _, error, _, _ in + XCTAssertNotNil(error) + XCTAssertTrue(NSThread.isMainThread()) + expectation.fulfill() + }) + waitForExpectationsWithTimeout(5, handler: nil) + } + + func testSucessCompletionHandlerRunningOnCustomQueue() { + let progressExpectation = expectationWithDescription("progressBlock running on custom queue") + let completionExpectation = expectationWithDescription("completionHandler running on custom queue") + let URLString = testKeys[0] + stubRequest("GET", URLString).andReturn(200).withBody(testImageData) + + let URL = NSURL(string: URLString)! + + let customQueue = dispatch_queue_create("com.kingfisher.testQueue", DISPATCH_QUEUE_SERIAL) + manager.retrieveImageWithURL(URL, optionsInfo: [.CallbackDispatchQueue(customQueue)], progressBlock: { _, _ in + XCTAssertTrue(NSThread.isMainThread()) + progressExpectation.fulfill() + }, completionHandler: { _, error, _, _ in + XCTAssertNil(error) + XCTAssertEqual(String(UTF8String: dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL))!, "com.kingfisher.testQueue") + completionExpectation.fulfill() + }) + waitForExpectationsWithTimeout(5, handler: nil) + } + + func testErrorCompletionHandlerRunningOnCustomQueue() { + let expectation = expectationWithDescription("running on custom queue") + let URLString = testKeys[0] + stubRequest("GET", URLString).andReturn(404) + + let URL = NSURL(string: URLString)! + + let customQueue = dispatch_queue_create("com.kingfisher.testQueue", DISPATCH_QUEUE_SERIAL) + manager.retrieveImageWithURL(URL, optionsInfo: [.CallbackDispatchQueue(customQueue)], progressBlock: { _, _ in + //won't be called + }, completionHandler: { _, error, _, _ in + XCTAssertNotNil(error) + XCTAssertEqual(String(UTF8String: dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL))!, "com.kingfisher.testQueue") + expectation.fulfill() + }) + waitForExpectationsWithTimeout(5, handler: nil) + } }