Skip to content

Commit

Permalink
Merge pull request onevcat#249 from krider2010/master
Browse files Browse the repository at this point in the history
Provide a simple prefetcher implementation.
  • Loading branch information
onevcat committed Mar 3, 2016
2 parents e174d46 + 1a24179 commit 1d39c83
Show file tree
Hide file tree
Showing 6 changed files with 355 additions and 1 deletion.
18 changes: 18 additions & 0 deletions Kingfisher.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@
D1ED2D401AD2D09F00CFC3EB /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1ED2D351AD2D09F00CFC3EB /* Kingfisher.framework */; };
D1ED2D4C1AD2D09F00CFC3EB /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1ED2D351AD2D09F00CFC3EB /* Kingfisher.framework */; };
D1ED2D4D1AD2D09F00CFC3EB /* Kingfisher.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D1ED2D351AD2D09F00CFC3EB /* Kingfisher.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
D9638BA01C7DBA660046523D /* ImagePrefetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9638B9F1C7DBA660046523D /* ImagePrefetcher.swift */; };
D9638BA11C7DBA660046523D /* ImagePrefetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9638B9F1C7DBA660046523D /* ImagePrefetcher.swift */; };
D9638BA21C7DBA660046523D /* ImagePrefetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9638B9F1C7DBA660046523D /* ImagePrefetcher.swift */; };
D9638BA31C7DBA660046523D /* ImagePrefetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9638B9F1C7DBA660046523D /* ImagePrefetcher.swift */; };
D9638BA61C7DC71F0046523D /* ImagePrefetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */; };
D9638BA71C7DCF560046523D /* ImagePrefetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */; };
D9638BA81C7DCF570046523D /* ImagePrefetcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -314,6 +321,8 @@
D1ED2D351AD2D09F00CFC3EB /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D1ED2D3F1AD2D09F00CFC3EB /* KingfisherTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KingfisherTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D809C0611AAB7CA1AE240862 /* Pods-KingfisherTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KingfisherTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-KingfisherTests/Pods-KingfisherTests.debug.xcconfig"; sourceTree = "<group>"; };
D9638B9F1C7DBA660046523D /* ImagePrefetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImagePrefetcher.swift; path = Sources/ImagePrefetcher.swift; sourceTree = "<group>"; };
D9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePrefetcherTests.swift; sourceTree = "<group>"; };
FE96DF45BEE5F8EBB01C7956 /* Pods-KingfisherTests-OSX.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KingfisherTests-OSX.release.xcconfig"; path = "Pods/Target Support Files/Pods-KingfisherTests-OSX/Pods-KingfisherTests-OSX.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -430,6 +439,7 @@
D10945EA1C526B6C001408EB /* Image.swift */,
D10945EB1C526B6C001408EB /* ImageCache.swift */,
D10945EC1C526B6C001408EB /* ImageDownloader.swift */,
D9638B9F1C7DBA660046523D /* ImagePrefetcher.swift */,
D10945ED1C526B6C001408EB /* ImageTransition.swift */,
D10945EE1C526B6C001408EB /* ImageView+Kingfisher.swift */,
D10945EF1C526B6C001408EB /* Info.plist */,
Expand Down Expand Up @@ -475,6 +485,7 @@
D12E0C451C47F23500AC98AD /* ImageCacheTests.swift */,
D12E0C461C47F23500AC98AD /* ImageDownloaderTests.swift */,
D12E0C471C47F23500AC98AD /* ImageExtensionTests.swift */,
D9638BA41C7DC71F0046523D /* ImagePrefetcherTests.swift */,
D12E0C481C47F23500AC98AD /* ImageViewExtensionTests.swift */,
D12E0C4A1C47F23500AC98AD /* KingfisherManagerTests.swift */,
D12E0C4B1C47F23500AC98AD /* KingfisherOptionsInfoTests.swift */,
Expand Down Expand Up @@ -1252,6 +1263,7 @@
D109461F1C526C61001408EB /* KingfisherManager.swift in Sources */,
D10946201C526C61001408EB /* KingfisherOptionsInfo.swift in Sources */,
D10946211C526C61001408EB /* Resource.swift in Sources */,
D9638BA21C7DBA660046523D /* ImagePrefetcher.swift in Sources */,
D10946221C526C61001408EB /* String+MD5.swift in Sources */,
D10946231C526C61001408EB /* ThreadHelper.swift in Sources */,
);
Expand All @@ -1273,6 +1285,7 @@
D12E0C761C47F71700AC98AD /* KingfisherTestHelper.swift in Sources */,
D12E0C6E1C47F6FE00AC98AD /* ImageCacheTests.swift in Sources */,
D12E0C6F1C47F6FE00AC98AD /* ImageDownloaderTests.swift in Sources */,
D9638BA71C7DCF560046523D /* ImagePrefetcherTests.swift in Sources */,
D12E0C701C47F6FE00AC98AD /* ImageExtensionTests.swift in Sources */,
D12E0C711C47F6FE00AC98AD /* ImageViewExtensionTests.swift in Sources */,
D12E0C721C47F6FE00AC98AD /* KingfisherManagerTests.swift in Sources */,
Expand All @@ -1288,6 +1301,7 @@
D12E0C891C47F7B700AC98AD /* KingfisherTestHelper.swift in Sources */,
D12E0C821C47F7AF00AC98AD /* ImageCacheTests.swift in Sources */,
D12E0C831C47F7AF00AC98AD /* ImageDownloaderTests.swift in Sources */,
D9638BA81C7DCF570046523D /* ImagePrefetcherTests.swift in Sources */,
D12E0C841C47F7AF00AC98AD /* ImageExtensionTests.swift in Sources */,
D12E0C851C47F7AF00AC98AD /* ImageViewExtensionTests.swift in Sources */,
D12E0C861C47F7AF00AC98AD /* KingfisherManagerTests.swift in Sources */,
Expand Down Expand Up @@ -1316,6 +1330,7 @@
D10946121C526C0D001408EB /* ImageView+Kingfisher.swift in Sources */,
D10946131C526C0D001408EB /* KingfisherManager.swift in Sources */,
D10946141C526C0D001408EB /* KingfisherOptionsInfo.swift in Sources */,
D9638BA11C7DBA660046523D /* ImagePrefetcher.swift in Sources */,
D10946151C526C0D001408EB /* Resource.swift in Sources */,
D10946161C526C0D001408EB /* String+MD5.swift in Sources */,
D10946171C526C0D001408EB /* ThreadHelper.swift in Sources */,
Expand All @@ -1330,6 +1345,7 @@
D109462D1C526CF5001408EB /* ImageTransition.swift in Sources */,
D10946251C526CE8001408EB /* Image.swift in Sources */,
D10946261C526CE8001408EB /* ImageCache.swift in Sources */,
D9638BA31C7DBA660046523D /* ImagePrefetcher.swift in Sources */,
D10946271C526CE8001408EB /* ImageDownloader.swift in Sources */,
D10946281C526CE8001408EB /* KingfisherManager.swift in Sources */,
D10946291C526CE8001408EB /* KingfisherOptionsInfo.swift in Sources */,
Expand Down Expand Up @@ -1369,6 +1385,7 @@
D10945FB1C526B86001408EB /* ImageView+Kingfisher.swift in Sources */,
D10945FC1C526B86001408EB /* KingfisherManager.swift in Sources */,
D10945FD1C526B86001408EB /* KingfisherOptionsInfo.swift in Sources */,
D9638BA01C7DBA660046523D /* ImagePrefetcher.swift in Sources */,
D10945FE1C526B86001408EB /* Resource.swift in Sources */,
D10945FF1C526B86001408EB /* String+MD5.swift in Sources */,
D10946001C526B86001408EB /* ThreadHelper.swift in Sources */,
Expand All @@ -1383,6 +1400,7 @@
D12E0C571C47F23500AC98AD /* KingfisherTestHelper.swift in Sources */,
D12E0C581C47F23500AC98AD /* UIButtonExtensionTests.swift in Sources */,
D12E0C561C47F23500AC98AD /* KingfisherOptionsInfoTests.swift in Sources */,
D9638BA61C7DC71F0046523D /* ImagePrefetcherTests.swift in Sources */,
D12E0C551C47F23500AC98AD /* KingfisherManagerTests.swift in Sources */,
D12E0C511C47F23500AC98AD /* ImageDownloaderTests.swift in Sources */,
D12E0C521C47F23500AC98AD /* ImageExtensionTests.swift in Sources */,
Expand Down
15 changes: 15 additions & 0 deletions Sources/ImageCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,21 @@ extension ImageCache {
public let cacheType: CacheType?
}

/**
Determine if a cached image exists for the given image, as keyed by the URL. It will return true if the
image is found either in memory or on disk. Essentially as long as there is a cache of the image somewhere
true is returned. A convenience method that decodes `isImageCachedForKey`.

- parameter url: The image URL.

- returns: True if the image is cached, false otherwise.
*/
public func cachedImageExistsforURL(url: NSURL) -> Bool {
let resource = Resource(downloadURL: url)
let result = isImageCachedForKey(resource.cacheKey)
return result.cached
}

/**
Check whether an image is cached for a key.

Expand Down
152 changes: 152 additions & 0 deletions Sources/ImagePrefetcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//
// ImagePrefetcher.swift
// Kingfisher
//
// Created by Claire Knight <[email protected]> on 24/02/2016
//
// Copyright (c) 2016 Wei Wang <[email protected]>
//
// 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.


#if os(OSX)
import AppKit
#else
import UIKit
#endif


/// Progress update block of prefetcher.
public typealias PrefetchProgressBlock = ((completedURLs: Int, allURLs: Int) -> ())

/// Completion block of prefetcher.
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 {

private var prefetchURLs: [NSURL]?
private var skippedCount = 0
private var requestedCount = 0
private var finishedCount = 0

private var cancelCompletionHandlerCalled = false

/// 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
of assets that are required for later use in an app. This code will not try and update any UI
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.
- parameter completionHandler: Block to be called when prefetching is complete. Completed is all
those made, and skipped is the number of failed ones.
*/
public func prefetchURLs(urls: [NSURL], progressBlock: PrefetchProgressBlock?, completionHandler: PrefetchCompletionBlock?) {

// Clear out any existing prefetch operation first
cancelPrefetching()

cancelCompletionHandlerCalled = false

prefetchURLs = urls

guard urls.count > 0 else {
CompletionHandler?()
return
}

for i in (0..<urls.count) where i < maxConcurrentDownloads && requestedCount < urls.count {
startPrefetching(i, progressBlock: progressBlock, completionHandler: completionHandler)
}
}

/**
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 }

requestedCount++

let task = RetrieveImageTask()
let resource = Resource(downloadURL: urls[index])
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: total)

// 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 {
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)
}
}
2 changes: 1 addition & 1 deletion Sources/KingfisherManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public class KingfisherManager {
/**
Default init method

- returns: A Kingfisher manager object with default cache and default downloader.
- returns: A Kingfisher manager object with default cache, default downloader, and default prefetcher.
*/
public init() {
cache = ImageCache.defaultCache
Expand Down
46 changes: 46 additions & 0 deletions Tests/KingfisherTests/ImageCacheTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,52 @@ class ImageCacheTests: XCTestCase {

waitForExpectationsWithTimeout(5, handler: nil)
}

func testCachedFileExists() {
let expectation = expectationWithDescription("cache does contain image")

let URLString = testKeys[0]
let URL = NSURL(string: URLString)!

let exists = cache.cachedImageExistsforURL(URL)
XCTAssertFalse(exists)

cache.retrieveImageForKey(URLString, options: nil, completionHandler: { (image, type) -> () in
XCTAssertNil(image, "Should not be cached yet")
XCTAssertEqual(type, nil)

self.cache.storeImage(testImage, forKey: URLString, toDisk: true) { () -> () in
self.cache.retrieveImageForKey(URLString, options: nil, completionHandler: { (image, type) -> () in
XCTAssertNotNil(image, "Should be cached (memory or disk)")
XCTAssertEqual(type, CacheType.Memory)

let exists = self.cache.cachedImageExistsforURL(URL)
XCTAssertTrue(exists, "Image should exist in the cache (memory or disk)")

self.cache.clearMemoryCache()
self.cache.retrieveImageForKey(URLString, options: nil, completionHandler: { (image, type) -> () in
XCTAssertNotNil(image, "Should be cached (disk)")
XCTAssertEqual(type, CacheType.Disk)

let exists = self.cache.cachedImageExistsforURL(URL)
XCTAssertTrue(exists, "Image should exist in the cache (disk)")

expectation.fulfill()
})
})
}
})

waitForExpectationsWithTimeout(5, handler: nil)
}

func testCachedFileDoesNotExist() {
let URLString = testKeys[0]
let URL = NSURL(string: URLString)!

let exists = cache.cachedImageExistsforURL(URL)
XCTAssertFalse(exists)
}

func testCachedImageIsFetchedSyncronouslyFromTheMemoryCache() {
cache.storeImage(testImage, forKey: testKeys[0], toDisk: false) { () -> () in
Expand Down
Loading

0 comments on commit 1d39c83

Please sign in to comment.