Browse Source

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 năm trước cách đây
mục cha
commit
3acddb2850

+ 47 - 14
Sources/ImagePrefetcher.swift

@@ -36,8 +36,9 @@
 public typealias PrefetchProgressBlock = ((completedURLs: Int, allURLs: Int) -> ())
 public typealias PrefetchProgressBlock = ((completedURLs: Int, allURLs: Int) -> ())
 
 
 /// Completion block of prefetcher.
 /// 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.
 /// `ImagePrefetcher` represents a downloading manager for requesting many images via URLs and caching them.
 public class ImagePrefetcher: NSObject {
 public class ImagePrefetcher: NSObject {
@@ -47,15 +48,18 @@ public class ImagePrefetcher: NSObject {
     private var requestedCount = 0
     private var requestedCount = 0
     private var finishedCount = 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
      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
      with the results of the process, but calls the handlers with the number cached etc. Failed
      images are just skipped.
      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 urls:              The list of URLs to prefetch
      - parameter progressBlock:     Block to be called when progress updates. Completed and total
      - parameter progressBlock:     Block to be called when progress updates. Completed and total
                                     counts are provided. Completed does not imply success.
                                     counts are provided. Completed does not imply success.
@@ -74,6 +81,8 @@ public class ImagePrefetcher: NSObject {
         // Clear out any existing prefetch operation first
         // Clear out any existing prefetch operation first
         cancelPrefetching()
         cancelPrefetching()
         
         
+        cancelCompletionHandlerCalled = false
+        
         prefetchURLs = urls
         prefetchURLs = urls
         
         
         guard urls.count > 0 else {
         guard urls.count > 0 else {
@@ -81,18 +90,34 @@ public class ImagePrefetcher: NSObject {
             return
             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)
             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
         prefetchURLs = .None
         skippedCount = 0
         skippedCount = 0
         requestedCount = 0
         requestedCount = 0
         finishedCount = 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?) {
     internal func startPrefetching(index: Int, progressBlock: PrefetchProgressBlock?, completionHandler: PrefetchCompletionBlock?) {
         guard let urls = prefetchURLs where index < (urls.count ?? 0) else { return }
         guard let urls = prefetchURLs where index < (urls.count ?? 0) else { return }
         
         
@@ -100,20 +125,28 @@ public class ImagePrefetcher: NSObject {
         
         
         let task = RetrieveImageTask()
         let task = RetrieveImageTask()
         let resource = Resource(downloadURL: urls[index])
         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++
             self.finishedCount++
             
             
             if image == .None {
             if image == .None {
                 self.skippedCount++
                 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)
                 self.startPrefetching(self.requestedCount, progressBlock: progressBlock, completionHandler: completionHandler)
             } else if self.finishedCount == self.requestedCount {
             } 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)
         }, options: nil)
     }
     }
 }
 }

+ 0 - 4
Sources/KingfisherManager.swift

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

+ 48 - 5
Tests/KingfisherTests/ImagePrefetcherTests.swift

@@ -29,7 +29,7 @@ import XCTest
 
 
 class ImagePrefetcherTests: XCTestCase {
 class ImagePrefetcherTests: XCTestCase {
 
 
-    var manager: KingfisherManager!
+    var prefetcher: ImagePrefetcher!
     
     
     override class func setUp() {
     override class func setUp() {
         super.setUp()
         super.setUp()
@@ -44,13 +44,13 @@ class ImagePrefetcherTests: XCTestCase {
     override func setUp() {
     override func setUp() {
         super.setUp()
         super.setUp()
         // Put setup code here. This method is called before the invocation of each test method in the class.
         // 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() {
     override func tearDown() {
         // Put teardown code here. This method is called after the invocation of each test method in the class.
         // Put teardown code here. This method is called after the invocation of each test method in the class.
         cleanDefaultCache()
         cleanDefaultCache()
-        manager = nil
+        prefetcher = nil
         super.tearDown()
         super.tearDown()
     }
     }
 
 
@@ -65,10 +65,11 @@ class ImagePrefetcherTests: XCTestCase {
 
 
         let total = urls.count
         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")
             XCTAssertEqual(allURLs, total, "total urls should match all those the prefetcher knows about")
-        }) { (completedURLs, skippedURLs) -> () in
+        }) { (cancelled, completedURLs, skippedURLs) -> () in
             expectation.fulfill()
             expectation.fulfill()
+            XCTAssertFalse(cancelled, "the prefetch should not have been cancelled")
             XCTAssertEqual(completedURLs, total, "all requests should have been completed, regardless of success")
             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!
             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)
             let cacheStatus = KingfisherManager.sharedManager.cache.isImageCachedForKey(Resource(downloadURL: urls[0]).cacheKey)
@@ -77,4 +78,46 @@ class ImagePrefetcherTests: XCTestCase {
         
         
         waitForExpectationsWithTimeout(5, handler: nil)
         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)
+    }
 }
 }