From 4921cf9b300f49148bfd657221e7c1bf4136f3e2 Mon Sep 17 00:00:00 2001 From: Jie Date: Fri, 15 Apr 2016 11:47:33 +1000 Subject: [PATCH] implement NSButton extension for OSX --- Kingfisher.xcodeproj/project.pbxproj | 8 + NSButton+Kingfisher.swift | 268 ++++++++++++++++++ .../NSButtonExtensionTests.swift | 158 +++++++++++ 3 files changed, 434 insertions(+) create mode 100644 NSButton+Kingfisher.swift create mode 100644 Tests/KingfisherTests/NSButtonExtensionTests.swift diff --git a/Kingfisher.xcodeproj/project.pbxproj b/Kingfisher.xcodeproj/project.pbxproj index b8e3b991c..e3448f65f 100644 --- a/Kingfisher.xcodeproj/project.pbxproj +++ b/Kingfisher.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 0D9C68098E20AB4F19D7C313 /* libPods-KingfisherTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A9E621E297FEFAD35D39C34E /* libPods-KingfisherTests.a */; }; + 185218B41CC07F6D00BD58DE /* NSButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 185218B31CC07F6D00BD58DE /* NSButton+Kingfisher.swift */; }; + 185218B61CC07F8300BD58DE /* NSButtonExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 185218B51CC07F8300BD58DE /* NSButtonExtensionTests.swift */; }; 4A54251331E840CB85C78FA8 /* libPods-KingfisherTests-OSX.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ECD18204CB0CD37B49F631 /* libPods-KingfisherTests-OSX.a */; }; 4B164AD01B8D556900768EC6 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B164ACE1B8D554200768EC6 /* CFNetwork.framework */; }; 4B2944641C3D03980088C3E7 /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B2944481C3D01B20088C3E7 /* Kingfisher.framework */; }; @@ -247,6 +249,8 @@ /* Begin PBXFileReference section */ 026040C607726792406566BB /* Pods-KingfisherTests-tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KingfisherTests-tvOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-KingfisherTests-tvOS/Pods-KingfisherTests-tvOS.release.xcconfig"; sourceTree = ""; }; + 185218B31CC07F6D00BD58DE /* NSButton+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSButton+Kingfisher.swift"; sourceTree = ""; }; + 185218B51CC07F8300BD58DE /* NSButtonExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSButtonExtensionTests.swift; sourceTree = ""; }; 4B164ACE1B8D554200768EC6 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; 4B2944481C3D01B20088C3E7 /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4B2944551C3D03880088C3E7 /* Kingfisher-OSX-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Kingfisher-OSX-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -450,6 +454,7 @@ D10945F41C526B6C001408EB /* String+MD5.swift */, D10945F51C526B6C001408EB /* ThreadHelper.swift */, D10945F61C526B6C001408EB /* UIButton+Kingfisher.swift */, + 185218B31CC07F6D00BD58DE /* NSButton+Kingfisher.swift */, ); name = Sources; sourceTree = ""; @@ -492,6 +497,7 @@ D12E0C4C1C47F23500AC98AD /* KingfisherTestHelper.swift */, D12E0C4D1C47F23500AC98AD /* KingfisherTests-Bridging-Header.h */, D12E0C4E1C47F23500AC98AD /* UIButtonExtensionTests.swift */, + 185218B51CC07F8300BD58DE /* NSButtonExtensionTests.swift */, ); name = KingfisherTests; path = Tests/KingfisherTests; @@ -1261,6 +1267,7 @@ D109461D1C526C61001408EB /* ImageTransition.swift in Sources */, D109461E1C526C61001408EB /* ImageView+Kingfisher.swift in Sources */, D109461F1C526C61001408EB /* KingfisherManager.swift in Sources */, + 185218B41CC07F6D00BD58DE /* NSButton+Kingfisher.swift in Sources */, D10946201C526C61001408EB /* KingfisherOptionsInfo.swift in Sources */, D10946211C526C61001408EB /* Resource.swift in Sources */, D9638BA21C7DBA660046523D /* ImagePrefetcher.swift in Sources */, @@ -1299,6 +1306,7 @@ buildActionMask = 2147483647; files = ( D12E0C891C47F7B700AC98AD /* KingfisherTestHelper.swift in Sources */, + 185218B61CC07F8300BD58DE /* NSButtonExtensionTests.swift in Sources */, D12E0C821C47F7AF00AC98AD /* ImageCacheTests.swift in Sources */, D12E0C831C47F7AF00AC98AD /* ImageDownloaderTests.swift in Sources */, D9638BA81C7DCF570046523D /* ImagePrefetcherTests.swift in Sources */, diff --git a/NSButton+Kingfisher.swift b/NSButton+Kingfisher.swift new file mode 100644 index 000000000..f84d43897 --- /dev/null +++ b/NSButton+Kingfisher.swift @@ -0,0 +1,268 @@ +// +// NSButton+Kingfisher.swift +// Kingfisher +// +// Created by Jie Zhang on 14/04/2016. +// +// Copyright (c) 2016 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +import AppKit + +// MARK: - Set Images +/** + * Set image to use from web. + */ +extension NSButton { + + /** + Set an image with a URL, a placeholder image, options, progress handler and completion handler. + + - parameter URL: The URL of image. + - parameter placeholderImage: A placeholder image when retrieving the image at URL. + - parameter optionsInfo: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more. + - parameter progressBlock: Called when the image downloading progress gets updated. + - 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, + placeholderImage: Image? = nil, + optionsInfo: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: CompletionHandler? = nil) -> RetrieveImageTask + { + return kf_setImageWithResource(Resource(downloadURL: URL), + placeholderImage: placeholderImage, + optionsInfo: optionsInfo, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + + /** + Set an image with a URL, a placeholder image, options, progress handler and completion handler. + + - parameter resource: Resource object contains information such as `cacheKey` and `downloadURL`. + - parameter placeholderImage: A placeholder image when retrieving the image at URL. + - parameter optionsInfo: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more. + - parameter progressBlock: Called when the image downloading progress gets updated. + - 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? = nil, + optionsInfo: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: CompletionHandler? = nil) -> RetrieveImageTask + { + image = placeholderImage + kf_setWebURL(resource.downloadURL) + let task = KingfisherManager.sharedManager.retrieveImageWithResource(resource, optionsInfo: optionsInfo, + progressBlock: { receivedSize, totalSize in + if let progressBlock = progressBlock { + progressBlock(receivedSize: receivedSize, totalSize: totalSize) + } + }, + completionHandler: {[weak self] image, error, cacheType, imageURL in + 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 + } + + sSelf.kf_setImageTask(nil) + + guard let image = image else { + completionHandler?(image: nil, error: error, cacheType: cacheType, imageURL: imageURL) + return + } + + sSelf.image = image + completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL) + } + }) + + kf_setImageTask(task) + return task + } + +} + + +// MARK: - Associated Object +private var lastURLKey: Void? +private var imageTaskKey: Void? + +extension NSButton { + /// Get the image URL binded to this image view. + public var kf_webURL: NSURL? { + return objc_getAssociatedObject(self, &lastURLKey) as? NSURL + } + + private func kf_setWebURL(URL: NSURL) { + objc_setAssociatedObject(self, &lastURLKey, URL, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + + private var kf_imageTask: RetrieveImageTask? { + return objc_getAssociatedObject(self, &imageTaskKey) as? RetrieveImageTask + } + + private func kf_setImageTask(task: RetrieveImageTask?) { + objc_setAssociatedObject(self, &imageTaskKey, task, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } +} + +/** + * Set alternate image to use from web. + */ +extension NSButton { + + /** + Set an alternateImage with a URL, a placeholder image, options, progress handler and completion handler. + + - parameter URL: The URL of image. + - parameter placeholderImage: A placeholder image when retrieving the image at URL. + - parameter optionsInfo: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more. + - parameter progressBlock: Called when the image downloading progress gets updated. + - 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_setAlternateImageWithURL(URL: NSURL, + placeholderImage: Image? = nil, + optionsInfo: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: CompletionHandler? = nil) -> RetrieveImageTask + { + return kf_setAlternateImageWithResource(Resource(downloadURL: URL), + placeholderImage: placeholderImage, + optionsInfo: optionsInfo, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + + /** + Set an alternateImage with a URL, a placeholder image, options, progress handler and completion handler. + + - parameter resource: Resource object contains information such as `cacheKey` and `downloadURL`. + - parameter placeholderImage: A placeholder image when retrieving the image at URL. + - parameter optionsInfo: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more. + - parameter progressBlock: Called when the image downloading progress gets updated. + - 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_setAlternateImageWithResource(resource: Resource, + placeholderImage: Image? = nil, + optionsInfo: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: CompletionHandler? = nil) -> RetrieveImageTask + { + alternateImage = placeholderImage + kf_setAlternateWebURL(resource.downloadURL) + let task = KingfisherManager.sharedManager.retrieveImageWithResource(resource, optionsInfo: optionsInfo, + progressBlock: { receivedSize, totalSize in + if let progressBlock = progressBlock { + progressBlock(receivedSize: receivedSize, totalSize: totalSize) + } + }, + completionHandler: {[weak self] image, error, cacheType, imageURL in + dispatch_async_safely_to_main_queue { + guard let sSelf = self where imageURL == sSelf.kf_alternateWebURL else { + completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL) + return + } + + sSelf.kf_setAlternateImageTask(nil) + + guard let image = image else { + completionHandler?(image: nil, error: error, cacheType: cacheType, imageURL: imageURL) + return + } + + sSelf.alternateImage = image + completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL) + } + }) + + kf_setImageTask(task) + return task + } +} + +private var lastAlternateURLKey: Void? +private var alternateImageTaskKey: Void? + +// MARK: - Runtime for NSButton alternateImage +extension NSButton { + /** + Get the alternate image URL binded to this button. + */ + + public var kf_alternateWebURL: NSURL? { + return objc_getAssociatedObject(self, &lastAlternateURLKey) as? NSURL + } + + private func kf_setAlternateWebURL(URL: NSURL) { + objc_setAssociatedObject(self, &lastAlternateURLKey, URL, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + + private var kf_alternateImageTask: RetrieveImageTask? { + return objc_getAssociatedObject(self, &alternateImageTaskKey) as? RetrieveImageTask + } + + private func kf_setAlternateImageTask(task: RetrieveImageTask?) { + objc_setAssociatedObject(self, &alternateImageTaskKey, task, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } +} + + +// MARK: - Cancel image download tasks. +extension NSButton { + /** + Cancel the image download task bounded to the image view if it is running. + Nothing will happen if the downloading has already finished. + */ + public func kf_cancelImageDownloadTask() { + kf_imageTask?.downloadTask?.cancel() + } + + public func kf_cancelAlternateImageDownloadTask() { + kf_imageTask?.downloadTask?.cancel() + } +} \ No newline at end of file diff --git a/Tests/KingfisherTests/NSButtonExtensionTests.swift b/Tests/KingfisherTests/NSButtonExtensionTests.swift new file mode 100644 index 000000000..49a2d9fa4 --- /dev/null +++ b/Tests/KingfisherTests/NSButtonExtensionTests.swift @@ -0,0 +1,158 @@ +// +// UIButtonExtensionTests.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/17. +// +// Copyright (c) 2016 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import AppKit +import XCTest +@testable import Kingfisher + +class NSButtonExtensionTests: XCTestCase { + + var button: NSButton! + + override class func setUp() { + super.setUp() + LSNocilla.sharedInstance().start() + } + + override class func tearDown() { + super.tearDown() + LSNocilla.sharedInstance().stop() + } + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + button = NSButton() + KingfisherManager.sharedManager.downloader = ImageDownloader(name: "testDownloader") + cleanDefaultCache() + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + LSNocilla.sharedInstance().clearStubs() + button = nil + + cleanDefaultCache() + + super.tearDown() + } + + func testDownloadAndSetImage() { + let expectation = expectationWithDescription("wait for downloading image") + + let URLString = testKeys[0] + stubRequest("GET", URLString).andReturn(200).withBody(testImageData) + let URL = NSURL(string: URLString)! + + var progressBlockIsCalled = false + + cleanDefaultCache() + + button.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in + progressBlockIsCalled = true + }) { (image, error, cacheType, imageURL) -> () in + expectation.fulfill() + + XCTAssert(progressBlockIsCalled, "progressBlock should be called at least once.") + XCTAssert(image != nil, "Downloaded image should exist.") + XCTAssert(image! == testImage, "Downloaded image should be the same as test image.") + XCTAssert(self.button.image! == testImage, "Downloaded image should be already set to the image for state") + XCTAssert(self.button.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. But now is: \(cacheType)") + } + waitForExpectationsWithTimeout(5, handler: nil) + } + + func testDownloadAndSetAlternateImage() { + let expectation = expectationWithDescription("wait for downloading image") + + let URLString = testKeys[0] + stubRequest("GET", URLString).andReturn(200).withBody(testImageData) + let URL = NSURL(string: URLString)! + + var progressBlockIsCalled = false + button.kf_setAlternateImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in + progressBlockIsCalled = true + }) { (image, error, cacheType, imageURL) -> () in + expectation.fulfill() + + XCTAssert(progressBlockIsCalled, "progressBlock should be called at least once.") + XCTAssert(image != nil, "Downloaded image should exist.") + XCTAssert(image! == testImage, "Downloaded image should be the same as test image.") + XCTAssert(self.button.alternateImage! == testImage, "Downloaded image should be already set to the image for state") + XCTAssert(self.button.kf_alternateWebURL == imageURL, "Web URL should equal to the downloaded url.") + XCTAssert(cacheType == .None, "cacheType should be .None since the image was just downloaded.") + + } + waitForExpectationsWithTimeout(5, handler: nil) + } + + func testCacnelImageTask() { + let expectation = expectationWithDescription("wait for downloading image") + + let URLString = testKeys[0] + let stub = stubRequest("GET", URLString).andReturn(200).withBody(testImageData).delay() + let URL = NSURL(string: URLString)! + + button.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in + XCTFail("Progress block should not be called.") + }) { (image, error, cacheType, imageURL) -> () in + XCTAssertNotNil(error) + XCTAssertEqual(error?.code, NSURLErrorCancelled) + + expectation.fulfill() + } + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) * 0.1)), dispatch_get_main_queue()) { () -> Void in + self.button.kf_cancelImageDownloadTask() + stub.go() + } + + waitForExpectationsWithTimeout(5, handler: nil) + } + + func testCacnelAlternateImageTask() { + let expectation = expectationWithDescription("wait for downloading image") + + let URLString = testKeys[0] + let stub = stubRequest("GET", URLString).andReturn(200).withBody(testImageData).delay() + let URL = NSURL(string: URLString)! + + button.kf_setAlternateImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in + XCTFail("Progress block should not be called.") + }) { (image, error, cacheType, imageURL) -> () in + XCTAssertNotNil(error) + XCTAssertEqual(error?.code, NSURLErrorCancelled) + + expectation.fulfill() + } + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) * 0.1)), dispatch_get_main_queue()) { () -> Void in + self.button.kf_cancelAlternateImageDownloadTask() + stub.go() + } + + waitForExpectationsWithTimeout(5, handler: nil) + } +}