Просмотр исходного кода

Merge pull request #184 from onevcat/fix/download-task

Add download task
Wei Wang 10 лет назад
Родитель
Сommit
db47d30fe8

+ 54 - 26
Kingfisher/ImageDownloader.swift

@@ -33,7 +33,14 @@ public typealias ImageDownloaderProgressBlock = DownloadProgressBlock
 public typealias ImageDownloaderCompletionHandler = ((image: UIImage?, error: NSError?, imageURL: NSURL?, originalData: NSData?) -> ())
 
 /// Download task.
-public typealias RetrieveImageDownloadTask = NSURLSessionDataTask
+public struct RetrieveImageDownloadTask {
+    let internalTask: NSURLSessionDataTask
+    weak var ownerDownloader: ImageDownloader?
+
+    public func cancel() {
+        ownerDownloader?.cancelDownloadingTask(self)
+    }
+}
 
 private let defaultDownloaderName = "default"
 private let downloaderBarrierName = "com.onevcat.Kingfisher.ImageDownloader.Barrier."
@@ -75,6 +82,8 @@ public class ImageDownloader: NSObject {
         var responseData = NSMutableData()
         var shouldDecode = false
         var scale = KingfisherManager.DefaultOptions.scale
+        var downloadTaskCount = 0
+        var downloadTask: RetrieveImageDownloadTask?
     }
     
     // MARK: - Public property
@@ -150,12 +159,14 @@ public extension ImageDownloader {
     - parameter URL:               Target URL.
     - parameter progressBlock:     Called when the download progress updated.
     - parameter completionHandler: Called when the download progress finishes.
+    
+    - returns: A downloading task. You could call `cancel` on it to stop the downloading process.
     */
     public func downloadImageWithURL(URL: NSURL,
                            progressBlock: ImageDownloaderProgressBlock?,
-                       completionHandler: ImageDownloaderCompletionHandler?)
+                       completionHandler: ImageDownloaderCompletionHandler?) -> RetrieveImageDownloadTask?
     {
-        downloadImageWithURL(URL, options: KingfisherManager.DefaultOptions, progressBlock: progressBlock, completionHandler: completionHandler)
+        return downloadImageWithURL(URL, options: KingfisherManager.DefaultOptions, progressBlock: progressBlock, completionHandler: completionHandler)
     }
     
     /**
@@ -165,13 +176,15 @@ public extension ImageDownloader {
     - parameter options:           The options could control download behavior. See `KingfisherManager.Options`
     - parameter progressBlock:     Called when the download progress updated.
     - parameter completionHandler: Called when the download progress finishes.
+
+    - returns: A downloading task. You could call `cancel` on it to stop the downloading process.
     */
     public func downloadImageWithURL(URL: NSURL,
                                  options: KingfisherManager.Options,
                            progressBlock: ImageDownloaderProgressBlock?,
-                       completionHandler: ImageDownloaderCompletionHandler?)
+                       completionHandler: ImageDownloaderCompletionHandler?) -> RetrieveImageDownloadTask?
     {
-        downloadImageWithURL(URL,
+        return downloadImageWithURL(URL,
             retrieveImageTask: nil,
                       options: options,
                 progressBlock: progressBlock,
@@ -182,10 +195,10 @@ public extension ImageDownloader {
                        retrieveImageTask: RetrieveImageTask?,
                                  options: KingfisherManager.Options,
                            progressBlock: ImageDownloaderProgressBlock?,
-                       completionHandler: ImageDownloaderCompletionHandler?)
+                       completionHandler: ImageDownloaderCompletionHandler?) -> RetrieveImageDownloadTask?
     {
-        if let retrieveImageTask = retrieveImageTask where retrieveImageTask.cancelled {
-            return
+        if let retrieveImageTask = retrieveImageTask where retrieveImageTask.cancelledBeforeDownlodStarting {
+            return nil
         }
         
         let timeout = self.downloadTimeout == 0.0 ? 15.0 : self.downloadTimeout
@@ -199,43 +212,58 @@ public extension ImageDownloader {
         // There is a possiblility that request modifier changed the url to `nil` or empty.
         if request.URL == nil || request.URL!.absoluteString.isEmpty {
             completionHandler?(image: nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.InvalidURL.rawValue, userInfo: nil), imageURL: nil, originalData: nil)
-            return
+            return nil
         }
         
+        var downloadTask: RetrieveImageDownloadTask?
         setupProgressBlock(progressBlock, completionHandler: completionHandler, forURL: request.URL!) {(session, fetchLoad) -> Void in
-            let task = session.dataTaskWithRequest(request)
-            task.priority = options.lowPriority ? NSURLSessionTaskPriorityLow : NSURLSessionTaskPriorityDefault
-            task.resume()
+            if fetchLoad.downloadTask == nil {
+                let dataTask = session.dataTaskWithRequest(request)
+                
+                fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self)
+                fetchLoad.shouldDecode = options.shouldDecode
+                fetchLoad.scale = options.scale
+                
+                dataTask.priority = options.lowPriority ? NSURLSessionTaskPriorityLow : NSURLSessionTaskPriorityDefault
+                dataTask.resume()
+            }
             
-            fetchLoad.shouldDecode = options.shouldDecode
-            fetchLoad.scale = options.scale
+            fetchLoad.downloadTaskCount += 1
+            downloadTask = fetchLoad.downloadTask
             
-            retrieveImageTask?.downloadTask = task
+            retrieveImageTask?.downloadTask = downloadTask
         }
+        return downloadTask
     }
     
     // A single key may have multiple callbacks. Only download once.
     internal func setupProgressBlock(progressBlock: ImageDownloaderProgressBlock?, completionHandler: ImageDownloaderCompletionHandler?, forURL URL: NSURL, started: ((NSURLSession, ImageFetchLoad) -> Void)) {
 
         dispatch_barrier_sync(barrierQueue, { () -> Void in
-
-            var create = false
-            var loadObjectForURL = self.fetchLoads[URL]
-            if  loadObjectForURL == nil {
-                create = true
-                loadObjectForURL = ImageFetchLoad()
-            }
             
+            let loadObjectForURL = self.fetchLoads[URL] ?? ImageFetchLoad()
             let callbackPair = (progressBlock: progressBlock, completionHander: completionHandler)
-            loadObjectForURL!.callbacks.append(callbackPair)
-            self.fetchLoads[URL] = loadObjectForURL!
             
-            if let session = self.session where create {
-                started(session, loadObjectForURL!)
+            loadObjectForURL.callbacks.append(callbackPair)
+            self.fetchLoads[URL] = loadObjectForURL
+            
+            if let session = self.session {
+                started(session, loadObjectForURL)
             }
         })
     }
     
+    func cancelDownloadingTask(task: RetrieveImageDownloadTask) {
+        dispatch_barrier_sync(barrierQueue) { () -> Void in
+            if let URL = task.internalTask.originalRequest?.URL, imageFetchLoad = self.fetchLoads[URL] {
+                imageFetchLoad.downloadTaskCount -= 1
+                if imageFetchLoad.downloadTaskCount == 0 {
+                    task.internalTask.cancel()
+                }
+            }
+        }
+    }
+    
     func cleanForURL(URL: NSURL) {
         dispatch_barrier_sync(barrierQueue, { () -> Void in
             self.fetchLoads.removeValueForKey(URL)

+ 3 - 3
Kingfisher/KingfisherManager.swift

@@ -35,7 +35,7 @@ public class RetrieveImageTask {
     
     // If task is canceled before the download task started (which means the `downloadTask` is nil),
     // the download task should not begin.
-    var cancelled: Bool = false
+    var cancelledBeforeDownlodStarting: Bool = false
     
     var diskRetrieveTask: RetrieveImageDiskTask?
     var downloadTask: RetrieveImageDownloadTask?
@@ -53,9 +53,9 @@ public class RetrieveImageTask {
         
         if let downloadTask = downloadTask {
             downloadTask.cancel()
+        } else {
+            cancelledBeforeDownlodStarting = true
         }
-        
-        cancelled = true
     }
 }
 

+ 36 - 0
KingfisherTests/ImageDownloaderTests.swift

@@ -219,4 +219,40 @@ class ImageDownloaderTests: XCTestCase {
         }
         waitForExpectationsWithTimeout(5, handler: nil)
     }
+    
+    func testCancelDownloadTask() {
+        
+        let expectation = expectationWithDescription("wait for downloading")
+        
+        let URLString = testKeys[0]
+        stubRequest("GET", URLString).andReturn(200).withBody(testImageData)
+        let URL = NSURL(string: URLString)!
+        
+        var progressBlockIsCalled = false
+        var completionBlockIsCalled = false
+        
+        let downloadTask = downloader.downloadImageWithURL(URL, progressBlock: { (receivedSize, totalSize) -> () in
+                progressBlockIsCalled = true
+            }) { (image, error, imageURL, originalData) -> () in
+                XCTAssertNotNil(error)
+                XCTAssertEqual(error!.code, NSURLErrorCancelled)
+                completionBlockIsCalled = true
+        }
+        
+        XCTAssertNotNil(downloadTask)
+        downloadTask!.cancel()
+        
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) * 0.09)), dispatch_get_main_queue()) { () -> Void in
+            expectation.fulfill()
+            XCTAssert(progressBlockIsCalled == false, "ProgressBlock should not be called since it is canceled.")
+            XCTAssert(completionBlockIsCalled == true, "CompletionBlock should be called with error.")
+        }
+        
+        waitForExpectationsWithTimeout(5, handler: nil)
+    }
+    
+    func testDownloadTaskNil() {
+        let downloadTask = downloader.downloadImageWithURL(NSURL(string: "")!, progressBlock: nil, completionHandler: nil)
+        XCTAssertNil(downloadTask)
+    }
 }

+ 73 - 15
KingfisherTests/UIImageViewExtensionTests.swift

@@ -153,24 +153,26 @@ class UIImageViewExtensionTests: XCTestCase {
         let task = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
             progressBlockIsCalled = true
             }) { (image, error, cacheType, imageURL) -> () in
+                XCTAssertNotNil(error)
+                XCTAssertEqual(error?.code, NSURLErrorCancelled)
                 completionBlockIsCalled = true
         }
         
-        dispatch_async(dispatch_get_main_queue()) { () -> Void in
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) * 0.1)), dispatch_get_main_queue()) { () -> Void in
             task.cancel()
             stub.go()
         }
 
-        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) * 0.09)), dispatch_get_main_queue()) { () -> Void in
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) * 0.2)), dispatch_get_main_queue()) { () -> Void in
             expectation.fulfill()
             XCTAssert(progressBlockIsCalled == false, "ProgressBlock should not be called since it is canceled.")
-            XCTAssert(completionBlockIsCalled == false, "CompletionBlock should not be called since it is canceled.")
+            XCTAssert(completionBlockIsCalled == true, "CompletionBlock should not be called since it is canceled.")
         }
         
         waitForExpectationsWithTimeout(5, handler: nil)
     }
 
-    func testImageDownloadCancelPartialTask() {
+    func testImageDownloadCancelPartialTaskBeforeRequest() {
         let expectation = expectationWithDescription("wait for downloading image")
         
         let URLString = testKeys[0]
@@ -190,20 +192,22 @@ class UIImageViewExtensionTests: XCTestCase {
         let _ = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
             
             }) { (image, error, cacheType, imageURL) -> () in
+                XCTAssertNotNil(image)
                 task2Completion = true
         }
         
         let _ = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
             
             }) { (image, error, cacheType, imageURL) -> () in
+                XCTAssertNotNil(image)
                 task3Completion = true
         }
         
         task1.cancel()
         
-        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) * 0.09)), dispatch_get_main_queue()) { () -> Void in
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) * 0.2)), dispatch_get_main_queue()) { () -> Void in
             expectation.fulfill()
-            XCTAssert(task1Completion == false, "Task 1 is canceled. The completion flag should be fasle.")
+            XCTAssert(task1Completion == false, "Task 1 should be not completed since it is cancelled before downloading started.")
             XCTAssert(task2Completion == true, "Task 2 should be completed.")
             XCTAssert(task3Completion == true, "Task 3 should be completed.")
         }
@@ -225,33 +229,32 @@ class UIImageViewExtensionTests: XCTestCase {
         let task1 = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
             
             }) { (image, error, cacheType, imageURL) -> () in
+                XCTAssertNotNil(image)
                 task1Completion = true
         }
         
-        let task2 = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
+        let _ = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
             
             }) { (image, error, cacheType, imageURL) -> () in
+                XCTAssertNotNil(image)
                 task2Completion = true
         }
         
-        let task3 = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
+        let _ = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
             
             }) { (image, error, cacheType, imageURL) -> () in
+                XCTAssertNotNil(image)
                 task3Completion = true
         }
         
-        // Prevent unused warning.
-        print(task2)
-        print(task3)
-        
-        dispatch_async(dispatch_get_main_queue()) { () -> Void in
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) * 0.1)), dispatch_get_main_queue()) { () -> Void in
             task1.cancel()
             stub.go()
         }
         
-        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) * 0.09)), dispatch_get_main_queue()) { () -> Void in
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) * 0.2)), dispatch_get_main_queue()) { () -> Void in
             expectation.fulfill()
-            XCTAssert(task1Completion == false, "Task 1 is canceled. The completion flag should be fasle.")
+            XCTAssert(task1Completion == true, "Task 1 should be completed since task 2 and 3 are not cancelled and they are sharing the same downloading process.")
             XCTAssert(task2Completion == true, "Task 2 should be completed.")
             XCTAssert(task3Completion == true, "Task 3 should be completed.")
         }
@@ -259,11 +262,66 @@ class UIImageViewExtensionTests: XCTestCase {
         waitForExpectationsWithTimeout(5, handler: nil)
     }
     
+    func testImageDownloadCancelAllTasksAfterRequestStarted() {
+        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)!
+        
+        var task1Completion = false
+        var task2Completion = false
+        var task3Completion = false
+        
+        let task1 = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
+            
+            }) { (image, error, cacheType, imageURL) -> () in
+                XCTAssertNotNil(error)
+                XCTAssertEqual(error?.code, NSURLErrorCancelled)
+                task1Completion = true
+        }
+        
+        let task2 = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
+            
+            }) { (image, error, cacheType, imageURL) -> () in
+                XCTAssertNotNil(error)
+                XCTAssertEqual(error?.code, NSURLErrorCancelled)
+                task2Completion = true
+        }
+        
+        let task3 = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
+            
+            }) { (image, error, cacheType, imageURL) -> () in
+                XCTAssertNotNil(error)
+                XCTAssertEqual(error?.code, NSURLErrorCancelled)
+                task3Completion = true
+        }
+        
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) * 0.1)), dispatch_get_main_queue()) { () -> Void in
+            task1.cancel()
+            task2.cancel()
+            task3.cancel()
+            stub.go()
+        }
+        
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) * 0.2)), dispatch_get_main_queue()) { () -> Void in
+            expectation.fulfill()
+            XCTAssert(task1Completion == true, "Task 1 should be completed with error.")
+            XCTAssert(task2Completion == true, "Task 2 should be completed with error.")
+            XCTAssert(task3Completion == true, "Task 3 should be completed with error.")
+        }
+        
+        waitForExpectationsWithTimeout(5, handler: nil)
+    }
+    
     func testImageDownalodMultipleCaches() {
         
         let cache1 = ImageCache(name: "cache1")
         let cache2 = ImageCache(name: "cache2")
         
+        cache1.clearDiskCache(true)
+        cache2.clearDiskCache(true)
+        
         let expectation = expectationWithDescription("wait for downloading image")
         
         let URLString = testKeys[0]