Forráskód Böngészése

Merge pull request #287 from jackyzh/master

implement NSButton extension for OSX
Wei Wang 9 éve
szülő
commit
e78bb40d83

+ 8 - 0
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 = "<group>"; };
+		185218B31CC07F6D00BD58DE /* NSButton+Kingfisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSButton+Kingfisher.swift"; sourceTree = "<group>"; };
+		185218B51CC07F8300BD58DE /* NSButtonExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSButtonExtensionTests.swift; sourceTree = "<group>"; };
 		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 = "<group>";
@@ -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 */,

+ 268 - 0
NSButton+Kingfisher.swift

@@ -0,0 +1,268 @@
+//
+//  NSButton+Kingfisher.swift
+//  Kingfisher
+//
+//  Created by Jie Zhang on 14/04/2016.
+//
+//  Copyright (c) 2016 Wei Wang <onevcat@gmail.com>
+//
+//  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()
+    }
+}

+ 158 - 0
Tests/KingfisherTests/NSButtonExtensionTests.swift

@@ -0,0 +1,158 @@
+//
+//  UIButtonExtensionTests.swift
+//  Kingfisher
+//
+//  Created by Wei Wang on 15/4/17.
+//
+//  Copyright (c) 2016 Wei Wang <onevcat@gmail.com>
+//
+//  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)
+    }
+}