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

Remove the deep relationship from the manager to the fetcher. Allow any to be specified, although a default is provided. Allow cancelling of requests too.

Claire Knight 10 лет назад
Родитель
Сommit
3acddb2850

+ 47 - 14
Sources/ImagePrefetcher.swift

@@ -36,8 +36,9 @@
 public typealias PrefetchProgressBlock = ((completedURLs: Int, allURLs: Int) -> ())
 
 /// Completion block of prefetcher.
-public typealias PrefetchCompletionBlock = ((completedURLs: Int, skippedURLs: Int) -> ())
+public typealias PrefetchCompletionBlock = ((cancelled: Bool, completedURLs: Int, skippedURLs: Int) -> ())
 
+private let defaultPrefetcherInstance = ImagePrefetcher()
 
 /// `ImagePrefetcher` represents a downloading manager for requesting many images via URLs and caching them.
 public class ImagePrefetcher: NSObject {
@@ -47,15 +48,18 @@ public class ImagePrefetcher: NSObject {
     private var requestedCount = 0
     private var finishedCount = 0
     
-    private var downloader: ImageDownloader
-
-    /// The maximum concurrent downloads to use when prefetching images. Default is 5.
-    var maxConcurrentDownloads = 5
+    private var cancelCompletionHandlerCalled = false
     
-    public init(downloader: ImageDownloader) {
-        self.downloader = downloader
-        super.init()
+    /// The default manager to use for downloads.
+    public lazy var manager: KingfisherManager = KingfisherManager.sharedManager
+
+    /// The default prefetcher.
+    public class var defaultPrefetcher: ImagePrefetcher {
+        return defaultPrefetcherInstance
     }
+
+    /// The maximum concurrent downloads to use when prefetching images. Default is 5.
+    public var maxConcurrentDownloads = 5
     
     /**
      Download the images from `urls` and cache them. This can be useful for background downloading
@@ -63,6 +67,9 @@ public class ImagePrefetcher: NSObject {
      with the results of the process, but calls the handlers with the number cached etc. Failed
      images are just skipped.
      
+     Warning: This will cancel any existing prefetch operation in progress! Use `isPrefetching() to
+     control this in your own code as you see fit.
+     
      - parameter urls:              The list of URLs to prefetch
      - parameter progressBlock:     Block to be called when progress updates. Completed and total
                                     counts are provided. Completed does not imply success.
@@ -74,6 +81,8 @@ public class ImagePrefetcher: NSObject {
         // Clear out any existing prefetch operation first
         cancelPrefetching()
         
+        cancelCompletionHandlerCalled = false
+        
         prefetchURLs = urls
         
         guard urls.count > 0 else {
@@ -81,18 +90,34 @@ public class ImagePrefetcher: NSObject {
             return
         }
         
-        for (var i = 0; i < maxConcurrentDownloads && requestedCount < urls.count; i++) {
+        for i in (0..<urls.count) where i < maxConcurrentDownloads && requestedCount < urls.count {
             startPrefetching(i, progressBlock: progressBlock, completionHandler: completionHandler)
         }
     }
    
-    internal func cancelPrefetching() {
+    /**
+     This cancels any existing prefetching activity that might be occuring. It does not stop any currently
+     running cache operation, but prevents any further ones being started and terminates the looping. For
+     surety, be sure that the completion block on the prefetch is called after calling this if you expect
+     an operation to be running.
+     */
+    func cancelPrefetching() {
         prefetchURLs = .None
         skippedCount = 0
         requestedCount = 0
         finishedCount = 0
     }
 
+    /**
+     Checks to see if this prefetcher is already prefetching any images.
+     
+     - returns: True if there are images still to be prefetched, false otherwise.
+     */
+    func isPrefetching() -> Bool {
+        guard let urls = prefetchURLs else { return false }
+        return urls.count > 0
+    }
+    
     internal func startPrefetching(index: Int, progressBlock: PrefetchProgressBlock?, completionHandler: PrefetchCompletionBlock?) {
         guard let urls = prefetchURLs where index < (urls.count ?? 0) else { return }
         
@@ -100,20 +125,28 @@ public class ImagePrefetcher: NSObject {
         
         let task = RetrieveImageTask()
         let resource = Resource(downloadURL: urls[index])
-        KingfisherManager.sharedManager.downloadAndCacheImageWithURL(resource.downloadURL, forKey: resource.cacheKey, retrieveImageTask: task, progressBlock: nil, completionHandler: { image, error, cacheType, imageURL in
+        let total = urls.count
+        
+        manager.downloadAndCacheImageWithURL(resource.downloadURL, forKey: resource.cacheKey, retrieveImageTask: task, progressBlock: nil, completionHandler: { image, error, cacheType, imageURL in
             self.finishedCount++
             
             if image == .None {
                 self.skippedCount++
             }
             
-            progressBlock?(completedURLs: self.finishedCount, allURLs: urls.count)
+            progressBlock?(completedURLs: self.finishedCount, allURLs: total)
             
-            if urls.count > self.requestedCount {
+            // Reference the prefetchURLs rather than urls in case the request has been cancelled
+            if (self.prefetchURLs?.count ?? 0) > self.requestedCount {
                 self.startPrefetching(self.requestedCount, progressBlock: progressBlock, completionHandler: completionHandler)
             } else if self.finishedCount == self.requestedCount {
-                completionHandler?(completedURLs: self.finishedCount, skippedURLs: self.skippedCount)
+                self.prefetchURLs?.removeAll()
+                completionHandler?(cancelled: false, completedURLs: self.finishedCount, skippedURLs: self.skippedCount)
+            } else if (self.prefetchURLs == nil || self.prefetchURLs!.count == 0) && !self.cancelCompletionHandlerCalled {
+                self.cancelCompletionHandlerCalled = true
+                completionHandler?(cancelled: true, completedURLs: self.finishedCount, skippedURLs: self.skippedCount)
             }
+            
         }, options: nil)
     }
 }

+ 0 - 4
Sources/KingfisherManager.swift

@@ -86,9 +86,6 @@ public class KingfisherManager {
     /// Downloader used by this manager
     public var downloader: ImageDownloader
     
-    /// Prefetched used by this manager
-    public var prefetcher: ImagePrefetcher
-    
     /**
     Default init method
     
@@ -97,7 +94,6 @@ public class KingfisherManager {
     public init() {
         cache = ImageCache.defaultCache
         downloader = ImageDownloader.defaultDownloader
-        prefetcher = ImagePrefetcher(downloader: downloader)
     }
     
     /**

+ 48 - 5
Tests/KingfisherTests/ImagePrefetcherTests.swift

@@ -29,7 +29,7 @@ import XCTest
 
 class ImagePrefetcherTests: XCTestCase {
 
-    var manager: KingfisherManager!
+    var prefetcher: ImagePrefetcher!
     
     override class func setUp() {
         super.setUp()
@@ -44,13 +44,13 @@ class ImagePrefetcherTests: XCTestCase {
     override func setUp() {
         super.setUp()
         // Put setup code here. This method is called before the invocation of each test method in the class.
-        manager = KingfisherManager()
+        prefetcher = ImagePrefetcher()
     }
     
     override func tearDown() {
         // Put teardown code here. This method is called after the invocation of each test method in the class.
         cleanDefaultCache()
-        manager = nil
+        prefetcher = nil
         super.tearDown()
     }
 
@@ -65,10 +65,11 @@ class ImagePrefetcherTests: XCTestCase {
 
         let total = urls.count
         
-        manager.prefetcher.prefetchURLs(urls, progressBlock: { (completedURLs, allURLs) -> () in
+        prefetcher.prefetchURLs(urls, progressBlock: { (completedURLs, allURLs) -> () in
             XCTAssertEqual(allURLs, total, "total urls should match all those the prefetcher knows about")
-        }) { (completedURLs, skippedURLs) -> () in
+        }) { (cancelled, completedURLs, skippedURLs) -> () in
             expectation.fulfill()
+            XCTAssertFalse(cancelled, "the prefetch should not have been cancelled")
             XCTAssertEqual(completedURLs, total, "all requests should have been completed, regardless of success")
             KingfisherManager.sharedManager.cache.clearMemoryCache()  // Remove from the Memory cache to ensure it is on disk!
             let cacheStatus = KingfisherManager.sharedManager.cache.isImageCachedForKey(Resource(downloadURL: urls[0]).cacheKey)
@@ -77,4 +78,46 @@ class ImagePrefetcherTests: XCTestCase {
         
         waitForExpectationsWithTimeout(5, handler: nil)
     }
+    
+    func testCancelPrefetching() {
+        let expectation = expectationWithDescription("wait for prefetching images")
+        
+        var urls = [NSURL]()
+        for URLString in testKeys {
+            stubRequest("GET", URLString).andReturn(200).withBody(testImageData)
+            urls.append(NSURL(string: URLString)!)
+        }
+        
+        prefetcher.maxConcurrentDownloads = 2
+        
+        prefetcher.prefetchURLs(urls, progressBlock: { (completedURLs, allURLs) -> () in
+            }) { (cancelled, completedURLs, skippedURLs) -> () in
+                XCTAssertTrue(cancelled, "the prefetch should have been cancelled")
+                // The completed and skipped URLs will depend on how far through the process the prefetch got before the cancel was called
+                expectation.fulfill()
+        }
+        prefetcher.cancelPrefetching()
+
+        waitForExpectationsWithTimeout(5, handler: nil)
+    }
+    
+    func testIsPrefetching() {
+        let expectation = expectationWithDescription("wait for prefetching images")
+        
+        var urls = [NSURL]()
+        for URLString in testKeys {
+            stubRequest("GET", URLString).andReturn(200).withBody(testImageData)
+            urls.append(NSURL(string: URLString)!)
+        }
+        
+        prefetcher.prefetchURLs(urls, progressBlock: { (completedURLs, allURLs) -> () in
+            XCTAssertTrue(self.prefetcher.isPrefetching(), "should be prefetching")
+            }) { (cancelled, completedURLs, skippedURLs) -> () in
+                XCTAssertFalse(cancelled, "the prefetch should not have been cancelled")
+                XCTAssertFalse(self.prefetcher.isPrefetching(), "should not be prefetching")
+                expectation.fulfill()
+        }
+        
+        waitForExpectationsWithTimeout(5, handler: nil)
+    }
 }