Jelajahi Sumber

Merge pull request #42 from onevcat/1.3.0

[WIP] 1.3.0
Wei Wang 10 tahun lalu
induk
melakukan
342c32aeea

+ 1 - 1
Kingfisher-Demo/ViewController.swift

@@ -59,7 +59,7 @@ extension ViewController: UICollectionViewDataSource {
         let cell = collectionView.dequeueReusableCellWithReuseIdentifier("collectionViewCell", forIndexPath: indexPath) as! CollectionViewCell
         let cell = collectionView.dequeueReusableCellWithReuseIdentifier("collectionViewCell", forIndexPath: indexPath) as! CollectionViewCell
         cell.cellImageView.kf_setImageWithURL(NSURL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/kingfisher-\(indexPath.row + 1).jpg")!, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
         cell.cellImageView.kf_setImageWithURL(NSURL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/kingfisher-\(indexPath.row + 1).jpg")!, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
             println("\(indexPath.row + 1): \(receivedSize)/\(totalSize)")
             println("\(indexPath.row + 1): \(receivedSize)/\(totalSize)")
-        }) { (image, error, imageURL) -> () in
+        }) { (image, error, cacheType, imageURL) -> () in
             println("\(indexPath.row + 1): Finished")
             println("\(indexPath.row + 1): Finished")
         }
         }
         
         

+ 1 - 1
Kingfisher.podspec

@@ -1,7 +1,7 @@
 Pod::Spec.new do |s|
 Pod::Spec.new do |s|
 
 
   s.name         = "Kingfisher"
   s.name         = "Kingfisher"
-  s.version      = "1.2.0"
+  s.version      = "1.3.0"
   s.summary      = "A lightweight and pure Swift implemented library for downloading and cacheing image from the web."
   s.summary      = "A lightweight and pure Swift implemented library for downloading and cacheing image from the web."
 
 
   s.description  = <<-DESC
   s.description  = <<-DESC

+ 41 - 1
Kingfisher/ImageCache.swift

@@ -26,6 +26,20 @@
 
 
 import Foundation
 import Foundation
 
 
+/**
+This notification will be sent when the disk cache got cleaned either there are cached files expired or the total size exceeding the max allowed size. The `clearDiskCache` method will not trigger this notification.
+
+The `object` of this notification is the `ImageCache` object which sends the notification.
+
+A list of removed hashes (files) could be retrieved by accessing the array under `KingfisherDiskCacheCleanedHashKey` key in `userInfo` of the notification object you received. By checking the array, you could know the hash codes of files are removed.
+*/
+public let KingfisherDidCleanDiskCacheNotification = "com.onevcat.Kingfisher.KingfisherDidCleanDiskCacheNotification"
+
+/**
+Key for array of cleaned hashes in `userInfo` of `KingfisherDidCleanDiskCacheNotification`.
+*/
+public let KingfisherDiskCacheCleanedHashKey = "com.onevcat.Kingfisher.cleanedHash"
+
 private let defaultCacheName = "default"
 private let defaultCacheName = "default"
 private let cacheReverseDNS = "com.onevcat.Kingfisher.ImageCache."
 private let cacheReverseDNS = "com.onevcat.Kingfisher.ImageCache."
 private let ioQueueName = "com.onevcat.Kingfisher.ImageCache.ioQueue."
 private let ioQueueName = "com.onevcat.Kingfisher.ImageCache.ioQueue."
@@ -43,9 +57,12 @@ Cache type of a cached image.
 - Disk:   The image is cached in disk.
 - Disk:   The image is cached in disk.
 */
 */
 public enum CacheType {
 public enum CacheType {
-    case Memory, Disk
+    case Memory, Disk, None
 }
 }
 
 
+/**
+*	`ImageCache` represents both the memory and disk cache system of Kingfisher. While a default image cache object will be used if you prefer the extension methods of Kingfisher, you can create your own cache object and configure it as your need. You should use an `ImageCache` object to manipulate memory and disk cache for Kingfisher.
+*/
 public class ImageCache {
 public class ImageCache {
 
 
     //Memory
     //Memory
@@ -412,6 +429,9 @@ extension ImageCache {
                     
                     
                     for fileURL in sortedFiles {
                     for fileURL in sortedFiles {
                         if (self.fileManager.removeItemAtURL(fileURL, error: nil)) {
                         if (self.fileManager.removeItemAtURL(fileURL, error: nil)) {
+                            
+                            URLsToDelete.append(fileURL)
+                            
                             if let fileSize = cachedFiles[fileURL]?[NSURLTotalFileAllocatedSizeKey] as? NSNumber {
                             if let fileSize = cachedFiles[fileURL]?[NSURLTotalFileAllocatedSizeKey] as? NSNumber {
                                 diskCacheSize -= fileSize.unsignedLongValue
                                 diskCacheSize -= fileSize.unsignedLongValue
                             }
                             }
@@ -424,6 +444,15 @@ extension ImageCache {
                 }
                 }
                 
                 
                 dispatch_async(dispatch_get_main_queue(), { () -> Void in
                 dispatch_async(dispatch_get_main_queue(), { () -> Void in
+                    
+                    if URLsToDelete.count != 0 {
+                        let cleanedHashes = URLsToDelete.map({ (url) -> String in
+                            return url.lastPathComponent!
+                        })
+                        
+                        NSNotificationCenter.defaultCenter().postNotificationName(KingfisherDidCleanDiskCacheNotification, object: self, userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes])
+                    }
+                    
                     if let completionHandler = completionHandler {
                     if let completionHandler = completionHandler {
                         completionHandler()
                         completionHandler()
                     }
                     }
@@ -496,6 +525,17 @@ public extension ImageCache {
         return CacheCheckResult(cached: false, cacheType: nil)
         return CacheCheckResult(cached: false, cacheType: nil)
     }
     }
     
     
+    /**
+    Get the hash for the key. This could be used for matching files.
+    
+    :param: key The key which is used for caching.
+    
+    :returns: Corresponding hash.
+    */
+    public func hashForKey(key: String) -> String {
+        return cacheFileNameForKey(key)
+    }
+    
     /**
     /**
     Calculate the disk size taken by cache. 
     Calculate the disk size taken by cache. 
     It is the total allocated size of the cached files in bytes.
     It is the total allocated size of the cached files in bytes.

+ 48 - 2
Kingfisher/ImageDownloader.swift

@@ -26,9 +26,13 @@
 
 
 import Foundation
 import Foundation
 
 
+/// Progress update block of downloader.
 public typealias ImageDownloaderProgressBlock = DownloadProgressBlock
 public typealias ImageDownloaderProgressBlock = DownloadProgressBlock
-public typealias ImageDownloaderCompletionHandler = CompletionHandler
 
 
+/// Completion block of downloader.
+public typealias ImageDownloaderCompletionHandler = ((image: UIImage?, error: NSError?, imageURL: NSURL?) -> ())
+
+/// Download task.
 public typealias RetrieveImageDownloadTask = NSURLSessionDataTask
 public typealias RetrieveImageDownloadTask = NSURLSessionDataTask
 
 
 private let defaultDownloaderName = "default"
 private let defaultDownloaderName = "default"
@@ -36,6 +40,24 @@ private let downloaderBarrierName = "com.onevcat.Kingfisher.ImageDownloader.Barr
 private let imageProcessQueueName = "com.onevcat.Kingfisher.ImageDownloader.Process."
 private let imageProcessQueueName = "com.onevcat.Kingfisher.ImageDownloader.Process."
 private let instance = ImageDownloader(name: defaultDownloaderName)
 private let instance = ImageDownloader(name: defaultDownloaderName)
 
 
+/**
+*	Protocol of `ImageDownloader`.
+*/
+@objc public protocol ImageDownloaderDelegate {
+    /**
+    Called when the `ImageDownloader` object successfully downloaded an image from specified URL.
+    
+    :param: downloader The `ImageDownloader` object finishes the downloading.
+    :param: image      Downloaded image.
+    :param: URL        URL of the original request URL.
+    :param: response   The response object of the downloading process.
+    */
+    optional func imageDownloader(downloader: ImageDownloader, didDownloadImage image: UIImage, forURL URL: NSURL, withResponse response: NSURLResponse)
+}
+
+/**
+*	`ImageDownloader` represents a downloading manager for requesting the image with a URL from server.
+*/
 public class ImageDownloader: NSObject {
 public class ImageDownloader: NSObject {
     
     
     class ImageFetchLoad {
     class ImageFetchLoad {
@@ -55,6 +77,9 @@ public class ImageDownloader: NSObject {
     /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this set will be ignored. You can use this set to specify the self-signed site.
     /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this set will be ignored. You can use this set to specify the self-signed site.
     public var trustedHosts: Set<String>?
     public var trustedHosts: Set<String>?
     
     
+    /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
+    public weak var delegate: ImageDownloaderDelegate?
+    
     // MARK: - Internal property
     // MARK: - Internal property
     let barrierQueue: dispatch_queue_t
     let barrierQueue: dispatch_queue_t
     let processQueue: dispatch_queue_t
     let processQueue: dispatch_queue_t
@@ -187,7 +212,9 @@ public extension ImageDownloader {
 
 
 // MARK: - NSURLSessionTaskDelegate
 // MARK: - NSURLSessionTaskDelegate
 extension ImageDownloader: NSURLSessionDataDelegate {
 extension ImageDownloader: NSURLSessionDataDelegate {
-    
+    /**
+    This method is exposed since the compiler requests. Do not call it.
+    */
     public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
     public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
         
         
         if let URL = dataTask.originalRequest.URL, callbackPairs = fetchLoads[URL]?.callbacks {
         if let URL = dataTask.originalRequest.URL, callbackPairs = fetchLoads[URL]?.callbacks {
@@ -198,6 +225,9 @@ extension ImageDownloader: NSURLSessionDataDelegate {
         completionHandler(NSURLSessionResponseDisposition.Allow)
         completionHandler(NSURLSessionResponseDisposition.Allow)
     }
     }
     
     
+    /**
+    This method is exposed since the compiler requests. Do not call it.
+    */
     public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
     public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
 
 
         if let URL = dataTask.originalRequest.URL, fetchLoad = fetchLoads[URL] {
         if let URL = dataTask.originalRequest.URL, fetchLoad = fetchLoads[URL] {
@@ -216,6 +246,9 @@ extension ImageDownloader: NSURLSessionDataDelegate {
         }
         }
     }
     }
     
     
+    /**
+    This method is exposed since the compiler requests. Do not call it.
+    */
     public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
     public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
         
         
         if let URL = task.originalRequest.URL {
         if let URL = task.originalRequest.URL {
@@ -228,6 +261,9 @@ extension ImageDownloader: NSURLSessionDataDelegate {
                     
                     
                     if let fetchLoad = self.fetchLoads[URL] {
                     if let fetchLoad = self.fetchLoads[URL] {
                         if let image = UIImage(data: fetchLoad.responseData) {
                         if let image = UIImage(data: fetchLoad.responseData) {
+                            
+                            self.delegate?.imageDownloader?(self, didDownloadImage: image, forURL: URL, withResponse: task.response!)
+                            
                             if fetchLoad.shouldDecode {
                             if fetchLoad.shouldDecode {
                                 self.callbackWithImage(image.kf_decodedImage(), error: nil, imageURL: URL)
                                 self.callbackWithImage(image.kf_decodedImage(), error: nil, imageURL: URL)
                             } else {
                             } else {
@@ -235,6 +271,13 @@ extension ImageDownloader: NSURLSessionDataDelegate {
                             }
                             }
                             
                             
                         } else {
                         } else {
+                            // If server response is 304 (Not Modified), inform the callback handler with NotModified error.
+                            // It should be handled to get an image from cache, which is response of a manager object.
+                            if let res = task.response as? NSHTTPURLResponse where res.statusCode == 304 {
+                                self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.NotModified.rawValue, userInfo: nil), imageURL: URL)
+                                return
+                            }
+                            
                             self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.BadData.rawValue, userInfo: nil), imageURL: URL)
                             self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.BadData.rawValue, userInfo: nil), imageURL: URL)
                         }
                         }
                     } else {
                     } else {
@@ -247,6 +290,9 @@ extension ImageDownloader: NSURLSessionDataDelegate {
         }
         }
     }
     }
 
 
+    /**
+    This method is exposed since the compiler requests. Do not call it.
+    */
     public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential!) -> Void) {
     public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential!) -> Void) {
 
 
         if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
         if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {

+ 20 - 3
Kingfisher/KingfisherManager.swift

@@ -59,6 +59,7 @@ The error code.
 */
 */
 public enum KingfisherError: Int {
 public enum KingfisherError: Int {
     case BadData = 10000
     case BadData = 10000
+    case NotModified = 10001
     case InvalidURL = 20000
     case InvalidURL = 20000
 }
 }
 
 
@@ -69,12 +70,17 @@ private let instance = KingfisherManager()
 */
 */
 public class KingfisherManager {
 public class KingfisherManager {
 
 
+    /**
+    *	Options to control some downloader and cache behaviors.
+    */
     public typealias Options = (forceRefresh: Bool, lowPriority: Bool, cacheMemoryOnly: Bool, shouldDecode: Bool)
     public typealias Options = (forceRefresh: Bool, lowPriority: Bool, cacheMemoryOnly: Bool, shouldDecode: Bool)
     
     
+    /// A preset option tuple with all value set to `false`.
     public static var OptionsNone: Options = {
     public static var OptionsNone: Options = {
         return (forceRefresh: false, lowPriority: false, cacheMemoryOnly: false, shouldDecode: false)
         return (forceRefresh: false, lowPriority: false, cacheMemoryOnly: false, shouldDecode: false)
     }()
     }()
     
     
+    /// Shared manager used by the extensions across Kingfisher.
     public class var sharedManager: KingfisherManager {
     public class var sharedManager: KingfisherManager {
         return instance
         return instance
     }
     }
@@ -153,7 +159,7 @@ public class KingfisherManager {
             } else {
             } else {
                 let diskTask = targetCache.retrieveImageForKey(key, options: options, completionHandler: { (image, cacheType) -> () in
                 let diskTask = targetCache.retrieveImageForKey(key, options: options, completionHandler: { (image, cacheType) -> () in
                     if image != nil {
                     if image != nil {
-                        completionHandler?(image: image, error: nil, imageURL: URL)
+                        completionHandler?(image: image, error: nil, cacheType:cacheType, imageURL: URL)
                     } else {
                     } else {
                         self.downloadAndCacheImageWithURL(URL,
                         self.downloadAndCacheImageWithURL(URL,
                             forKey: key,
                             forKey: key,
@@ -185,7 +191,18 @@ public class KingfisherManager {
             progressBlock?(receivedSize: receivedSize, totalSize: totalSize)
             progressBlock?(receivedSize: receivedSize, totalSize: totalSize)
             return
             return
         }) { (image, error, imageURL) -> () in
         }) { (image, error, imageURL) -> () in
-            completionHandler?(image: image, error: error, imageURL: URL)
+
+            if let error = error where error.code == KingfisherError.NotModified.rawValue {
+                // Not modified. Try to find the image from cache. 
+                // (The image should be in cache. It should be ensured by the framework users.)
+                targetCache.retrieveImageForKey(key, options: options, completionHandler: { (cacheImage, cacheType) -> () in
+                    completionHandler?(image: cacheImage, error: nil, cacheType: cacheType, imageURL: URL)
+
+                })
+                return
+            }
+            
+            completionHandler?(image: image, error: error, cacheType: .None, imageURL: URL)
             if let image = image {
             if let image = image {
                 targetCache.storeImage(image, forKey: key, toDisk: !options.cacheMemoryOnly, completionHandler: nil)
                 targetCache.storeImage(image, forKey: key, toDisk: !options.cacheMemoryOnly, completionHandler: nil)
             }
             }
@@ -203,4 +220,4 @@ public extension KingfisherManager {
     {
     {
         return retrieveImageWithURL(URL, optionsInfo: [.Options : options], progressBlock: progressBlock, completionHandler: completionHandler)
         return retrieveImageWithURL(URL, optionsInfo: [.Options : options], progressBlock: progressBlock, completionHandler: completionHandler)
     }
     }
-}
+}

+ 23 - 2
Kingfisher/KingfisherOptions.swift

@@ -33,19 +33,40 @@ public struct KingfisherOptions : RawOptionSetType {
     typealias RawValue = UInt
     typealias RawValue = UInt
     private var value: UInt = 0
     private var value: UInt = 0
     init(_ value: UInt) { self.value = value }
     init(_ value: UInt) { self.value = value }
+    
+    /**
+    Init an option
+    
+    :param: value Raw value of the option.
+    
+    :returns: An option represets input value.
+    */
     public init(rawValue value: UInt) { self.value = value }
     public init(rawValue value: UInt) { self.value = value }
+    
+    /**
+    Init a None option
+    
+    :param: nilLiteral Void.
+    
+    :returns: An option represents None.
+    */
     public init(nilLiteral: ()) { self.value = 0 }
     public init(nilLiteral: ()) { self.value = 0 }
+    
+    /// An option represents None.
     public static var allZeros: KingfisherOptions { return self(0) }
     public static var allZeros: KingfisherOptions { return self(0) }
-    static func fromMask(raw: UInt) -> KingfisherOptions { return self(raw) }
+    
+    /// Raw value of the option.
     public var rawValue: UInt { return self.value }
     public var rawValue: UInt { return self.value }
     
     
+    static func fromMask(raw: UInt) -> KingfisherOptions { return self(raw) }
+
     /// None options. Kingfisher will keep its default behavior.
     /// None options. Kingfisher will keep its default behavior.
     public static var None: KingfisherOptions { return self(0) }
     public static var None: KingfisherOptions { return self(0) }
     
     
     /// Download in a low priority.
     /// Download in a low priority.
     public static var LowPriority: KingfisherOptions { return KingfisherOptions(1 << 0) }
     public static var LowPriority: KingfisherOptions { return KingfisherOptions(1 << 0) }
     
     
-    /// Ignore cache. Always download the image and cache it again.
+    /// Try to send request to server first. If response code is 304 (Not Modified), use the cached image. Otherwise, download the image and cache it again.
     public static var ForceRefresh: KingfisherOptions { return KingfisherOptions(1 << 1) }
     public static var ForceRefresh: KingfisherOptions { return KingfisherOptions(1 << 1) }
     
     
     /// Only cache downloaded image to memory, not cache in disk.
     /// Only cache downloaded image to memory, not cache in disk.

+ 4 - 4
Kingfisher/UIButton+Kingfisher.swift

@@ -127,12 +127,12 @@ public extension UIButton {
                     progressBlock(receivedSize: receivedSize, totalSize: totalSize)
                     progressBlock(receivedSize: receivedSize, totalSize: totalSize)
                 })
                 })
             }
             }
-        }) { (image, error, imageURL) -> () in
+        }) { (image, error, cacheType, imageURL) -> () in
             dispatch_async(dispatch_get_main_queue(), { () -> Void in
             dispatch_async(dispatch_get_main_queue(), { () -> Void in
                 if (imageURL == self.kf_webURLForState(state) && image != nil) {
                 if (imageURL == self.kf_webURLForState(state) && image != nil) {
                     self.setImage(image, forState: state)
                     self.setImage(image, forState: state)
                 }
                 }
-                completionHandler?(image: image, error: error, imageURL: imageURL)
+                completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL)
             })
             })
         }
         }
         
         
@@ -275,12 +275,12 @@ public extension UIButton {
                     progressBlock(receivedSize: receivedSize, totalSize: totalSize)
                     progressBlock(receivedSize: receivedSize, totalSize: totalSize)
                 })
                 })
             }
             }
-            }) { (image, error, imageURL) -> () in
+            }) { (image, error, cacheType, imageURL) -> () in
                 dispatch_async(dispatch_get_main_queue(), { () -> Void in
                 dispatch_async(dispatch_get_main_queue(), { () -> Void in
                     if (imageURL == self.kf_backgroundWebURLForState(state) && image != nil) {
                     if (imageURL == self.kf_backgroundWebURLForState(state) && image != nil) {
                         self.setBackgroundImage(image, forState: state)
                         self.setBackgroundImage(image, forState: state)
                     }
                     }
-                    completionHandler?(image: image, error: error, imageURL: imageURL)
+                    completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL)
                 })
                 })
         }
         }
         
         

+ 3 - 3
Kingfisher/UIImageView+Kingfisher.swift

@@ -27,7 +27,7 @@
 import Foundation
 import Foundation
 
 
 public typealias DownloadProgressBlock = ((receivedSize: Int64, totalSize: Int64) -> ())
 public typealias DownloadProgressBlock = ((receivedSize: Int64, totalSize: Int64) -> ())
-public typealias CompletionHandler = ((image: UIImage?, error: NSError?, imageURL: NSURL?) -> ())
+public typealias CompletionHandler = ((image: UIImage?, error: NSError?, cacheType:CacheType, imageURL: NSURL?) -> ())
 
 
 // MARK: - Set Images
 // MARK: - Set Images
 /**
 /**
@@ -122,12 +122,12 @@ public extension UIImageView {
                     progressBlock(receivedSize: receivedSize, totalSize: totalSize)
                     progressBlock(receivedSize: receivedSize, totalSize: totalSize)
                 })
                 })
             }
             }
-        }) { (image, error, imageURL) -> () in
+        }) { (image, error, cacheType, imageURL) -> () in
             dispatch_async(dispatch_get_main_queue(), { () -> Void in
             dispatch_async(dispatch_get_main_queue(), { () -> Void in
                 if (imageURL == self.kf_webURL && image != nil) {
                 if (imageURL == self.kf_webURL && image != nil) {
                     self.image = image;
                     self.image = image;
                 }
                 }
-                completionHandler?(image: image, error: error, imageURL: imageURL)
+                completionHandler?(image: image, error: error, cacheType:cacheType, imageURL: imageURL)
             })
             })
         }
         }
         
         

+ 27 - 2
KingfisherTests/ImageCacheTests.swift

@@ -34,6 +34,7 @@ private let cacheName = "com.onevcat.Kingfisher.ImageCache.test"
 class ImageCacheTests: XCTestCase {
 class ImageCacheTests: XCTestCase {
 
 
     var cache: ImageCache!
     var cache: ImageCache!
+    var observer: NSObjectProtocol!
     
     
     override func setUp() {
     override func setUp() {
         super.setUp()
         super.setUp()
@@ -46,6 +47,7 @@ class ImageCacheTests: XCTestCase {
         super.tearDown()
         super.tearDown()
         cache.clearDiskCache()
         cache.clearDiskCache()
         cache = nil
         cache = nil
+        observer = nil
     }
     }
     
     
     func testMaxCachePeriodInSecond() {
     func testMaxCachePeriodInSecond() {
@@ -164,12 +166,35 @@ class ImageCacheTests: XCTestCase {
                     self.cache.retrieveImageInDiskCacheForKey(testKeys[0])
                     self.cache.retrieveImageInDiskCacheForKey(testKeys[0])
                 }
                 }
             })
             })
-        
             expectation.fulfill()
             expectation.fulfill()
+        }
+        
+        self.waitForExpectationsWithTimeout(15, handler: nil)
+    }
+    
+    func testCleanDiskCacheNotification() {
+        let expectation = expectationWithDescription("wait for retriving image")
+        
+        cache.storeImage(testImage, forKey: testKeys[0], toDisk: true) { () -> () in
+
+            self.observer = NSNotificationCenter.defaultCenter().addObserverForName(KingfisherDidCleanDiskCacheNotification, object: self.cache, queue: NSOperationQueue.mainQueue(), usingBlock: { (noti) -> Void in
+
+                XCTAssert(noti.object === self.cache, "The object of notification should be the cache object.")
+                
+                let hashes = noti.userInfo?[KingfisherDiskCacheCleanedHashKey] as! [String]
+                
+                XCTAssertEqual(1, hashes.count, "There should be one and only one file cleaned")
+                XCTAssertEqual(hashes.first!, self.cache.hashForKey(testKeys[0]), "The cleaned file should be the stored one.")
+                
+                NSNotificationCenter.defaultCenter().removeObserver(self.observer)
+                expectation.fulfill()
+            })
             
             
+            self.cache.maxCachePeriodInSecond = 0
+            self.cache.cleanExpiredDiskCache()
         }
         }
         
         
-        self.waitForExpectationsWithTimeout(5, handler: nil)
+        waitForExpectationsWithTimeout(1, handler: nil)
     }
     }
 
 
     // MARK: - Helper
     // MARK: - Helper

+ 17 - 1
KingfisherTests/ImageDownloaderTests.swift

@@ -144,6 +144,22 @@ class ImageDownloaderTests: XCTestCase {
         waitForExpectationsWithTimeout(1, handler: nil)
         waitForExpectationsWithTimeout(1, handler: nil)
     }
     }
     
     
+    func testServerNotModifiedResponse() {
+        let expectation = expectationWithDescription("wait for server response 304")
+        
+        let URLString = testKeys[0]
+        stubRequest("GET", URLString).andReturn(304)
+        
+        downloader.downloadImageWithURL(NSURL(string: URLString)!, options: KingfisherManager.OptionsNone, progressBlock: { (receivedSize, totalSize) -> () in
+            
+        }) { (image, error, imageURL) -> () in
+            XCTAssertNotNil(error, "There should be an error since server returning 304 and no image downloaded.")
+            XCTAssertEqual(error!.code, KingfisherError.NotModified.rawValue, "The error should be NotModified.")
+            expectation.fulfill()
+        }
+        waitForExpectationsWithTimeout(1, handler: nil)
+    }
+    
     // Since we could not receive one challage, no test for trusted hosts currently.
     // Since we could not receive one challage, no test for trusted hosts currently.
     // See http://stackoverflow.com/questions/27065372/why-is-a-https-nsurlsession-connection-only-challenged-once-per-domain for more.
     // See http://stackoverflow.com/questions/27065372/why-is-a-https-nsurlsession-connection-only-challenged-once-per-domain for more.
     func testSSLCertificateValidation() {
     func testSSLCertificateValidation() {
@@ -155,7 +171,7 @@ class ImageDownloaderTests: XCTestCase {
         
         
         downloader.downloadImageWithURL(URL, progressBlock: nil, completionHandler: { (image, error, imageURL) -> () in
         downloader.downloadImageWithURL(URL, progressBlock: nil, completionHandler: { (image, error, imageURL) -> () in
             XCTAssertNotNil(error, "Error should not be nil")
             XCTAssertNotNil(error, "Error should not be nil")
-            XCTAssert(error?.code == NSURLErrorServerCertificateUntrusted, "Error should be NSURLErrorServerCertificateUntrusted")
+            XCTAssert(error?.code == NSURLErrorServerCertificateUntrusted, "Error should be NSURLErrorServerCertificateUntrusted, but \(error)")
             expectation.fulfill()
             expectation.fulfill()
             LSNocilla.sharedInstance().start()
             LSNocilla.sharedInstance().start()
         })
         })

+ 6 - 2
KingfisherTests/UIButtonExtensionTests.swift

@@ -53,6 +53,7 @@ class UIButtonExtensionTests: XCTestCase {
         // 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.
         LSNocilla.sharedInstance().clearStubs()
         LSNocilla.sharedInstance().clearStubs()
         button = nil
         button = nil
+        
         cleanDefaultCache()
         cleanDefaultCache()
         
         
         super.tearDown()
         super.tearDown()
@@ -68,7 +69,7 @@ class UIButtonExtensionTests: XCTestCase {
         var progressBlockIsCalled = false
         var progressBlockIsCalled = false
         button.kf_setImageWithURL(URL, forState: UIControlState.Highlighted, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
         button.kf_setImageWithURL(URL, forState: UIControlState.Highlighted, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
             progressBlockIsCalled = true
             progressBlockIsCalled = true
-        }) { (image, error, imageURL) -> () in
+        }) { (image, error, cacheType, imageURL) -> () in
             expectation.fulfill()
             expectation.fulfill()
             
             
             XCTAssert(progressBlockIsCalled, "progressBlock should be called at least once.")
             XCTAssert(progressBlockIsCalled, "progressBlock should be called at least once.")
@@ -76,6 +77,7 @@ class UIButtonExtensionTests: XCTestCase {
             XCTAssert(image! == testImage, "Downloaded image should be the same as test image.")
             XCTAssert(image! == testImage, "Downloaded image should be the same as test image.")
             XCTAssert(self.button.imageForState(UIControlState.Highlighted)! == testImage, "Downloaded image should be already set to the image for state")
             XCTAssert(self.button.imageForState(UIControlState.Highlighted)! == testImage, "Downloaded image should be already set to the image for state")
             XCTAssert(self.button.kf_webURLForState(UIControlState.Highlighted) == imageURL, "Web URL should equal to the downloaded url.")
             XCTAssert(self.button.kf_webURLForState(UIControlState.Highlighted) == imageURL, "Web URL should equal to the downloaded url.")
+            XCTAssert(cacheType == .None, "cacheType should be .None since the image was just downloaded.")
         }
         }
         waitForExpectationsWithTimeout(1, handler: nil)
         waitForExpectationsWithTimeout(1, handler: nil)
     }
     }
@@ -90,7 +92,7 @@ class UIButtonExtensionTests: XCTestCase {
         var progressBlockIsCalled = false
         var progressBlockIsCalled = false
         button.kf_setBackgroundImageWithURL(URL, forState: UIControlState.Normal, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
         button.kf_setBackgroundImageWithURL(URL, forState: UIControlState.Normal, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
             progressBlockIsCalled = true
             progressBlockIsCalled = true
-            }) { (image, error, imageURL) -> () in
+            }) { (image, error, cacheType, imageURL) -> () in
                 expectation.fulfill()
                 expectation.fulfill()
                 
                 
                 XCTAssert(progressBlockIsCalled, "progressBlock should be called at least once.")
                 XCTAssert(progressBlockIsCalled, "progressBlock should be called at least once.")
@@ -98,6 +100,8 @@ class UIButtonExtensionTests: XCTestCase {
                 XCTAssert(image! == testImage, "Downloaded image should be the same as test image.")
                 XCTAssert(image! == testImage, "Downloaded image should be the same as test image.")
                 XCTAssert(self.button.backgroundImageForState(UIControlState.Normal)! == testImage, "Downloaded image should be already set to the image for state")
                 XCTAssert(self.button.backgroundImageForState(UIControlState.Normal)! == testImage, "Downloaded image should be already set to the image for state")
                 XCTAssert(self.button.kf_backgroundWebURLForState(UIControlState.Normal) == imageURL, "Web URL should equal to the downloaded url.")
                 XCTAssert(self.button.kf_backgroundWebURLForState(UIControlState.Normal) == imageURL, "Web URL should equal to the downloaded url.")
+                XCTAssert(cacheType == .None, "cacheType should be .None since the image was just downloaded.")
+
         }
         }
         waitForExpectationsWithTimeout(1, handler: nil)
         waitForExpectationsWithTimeout(1, handler: nil)
     }
     }

+ 9 - 7
KingfisherTests/UIImageViewExtensionTests.swift

@@ -70,7 +70,7 @@ class UIImageViewExtensionTests: XCTestCase {
         
         
         imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
         imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
             progressBlockIsCalled = true
             progressBlockIsCalled = true
-        }) { (image, error, imageURL) -> () in
+        }) { (image, error, cacheType, imageURL) -> () in
             expectation.fulfill()
             expectation.fulfill()
             
             
             XCTAssert(progressBlockIsCalled, "progressBlock should be called at least once.")
             XCTAssert(progressBlockIsCalled, "progressBlock should be called at least once.")
@@ -78,6 +78,8 @@ class UIImageViewExtensionTests: XCTestCase {
             XCTAssert(image! == testImage, "Downloaded image should be the same as test image.")
             XCTAssert(image! == testImage, "Downloaded image should be the same as test image.")
             XCTAssert(self.imageView.image! == testImage, "Downloaded image should be already set to the image property.")
             XCTAssert(self.imageView.image! == testImage, "Downloaded image should be already set to the image property.")
             XCTAssert(self.imageView.kf_webURL == imageURL, "Web URL should equal to the downloaded url.")
             XCTAssert(self.imageView.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.")
         }
         }
         
         
         waitForExpectationsWithTimeout(1, handler: nil)
         waitForExpectationsWithTimeout(1, handler: nil)
@@ -95,7 +97,7 @@ class UIImageViewExtensionTests: XCTestCase {
         
         
         let task = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
         let task = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
             progressBlockIsCalled = true
             progressBlockIsCalled = true
-        }) { (image, error, imageURL) -> () in
+        }) { (image, error, cacheType, imageURL) -> () in
             completionBlockIsCalled = true
             completionBlockIsCalled = true
         }
         }
         task.cancel()
         task.cancel()
@@ -122,19 +124,19 @@ class UIImageViewExtensionTests: XCTestCase {
         
         
         let task1 = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
         let task1 = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
 
 
-            }) { (image, error, imageURL) -> () in
+            }) { (image, error, cacheType, imageURL) -> () in
                 task1Completion = true
                 task1Completion = true
         }
         }
         
         
         let task2 = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
         let task2 = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
             
             
-            }) { (image, error, imageURL) -> () in
+            }) { (image, error, cacheType, imageURL) -> () in
                 task2Completion = true
                 task2Completion = true
         }
         }
         
         
         let task3 = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
         let task3 = imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: nil, progressBlock: { (receivedSize, totalSize) -> () in
             
             
-            }) { (image, error, imageURL) -> () in
+            }) { (image, error, cacheType, imageURL) -> () in
                 task3Completion = true
                 task3Completion = true
         }
         }
         
         
@@ -163,7 +165,7 @@ class UIImageViewExtensionTests: XCTestCase {
         
         
         imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: [.TargetCache: cache1], progressBlock: { (receivedSize, totalSize) -> () in
         imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: [.TargetCache: cache1], progressBlock: { (receivedSize, totalSize) -> () in
             
             
-        }) { (image, error, imageURL) -> () in
+        }) { (image, error, cacheType, imageURL) -> () in
             
             
             XCTAssertTrue(cache1.isImageCachedForKey(URLString).cached, "This image should be cached in cache1.")
             XCTAssertTrue(cache1.isImageCachedForKey(URLString).cached, "This image should be cached in cache1.")
             XCTAssertFalse(cache2.isImageCachedForKey(URLString).cached, "This image should not be cached in cache2.")
             XCTAssertFalse(cache2.isImageCachedForKey(URLString).cached, "This image should not be cached in cache2.")
@@ -171,7 +173,7 @@ class UIImageViewExtensionTests: XCTestCase {
             
             
             self.imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: [.TargetCache: cache2], progressBlock: { (receivedSize, totalSize) -> () in
             self.imageView.kf_setImageWithURL(URL, placeholderImage: nil, optionsInfo: [.TargetCache: cache2], progressBlock: { (receivedSize, totalSize) -> () in
                 
                 
-            }, completionHandler: { (image, error, imageURL) -> () in
+            }, completionHandler: { (image, error, cacheType, imageURL) -> () in
                 XCTAssertTrue(cache1.isImageCachedForKey(URLString).cached, "This image should be cached in cache1.")
                 XCTAssertTrue(cache1.isImageCachedForKey(URLString).cached, "This image should be cached in cache1.")
                 XCTAssertTrue(cache2.isImageCachedForKey(URLString).cached, "This image should be cached in cache2.")
                 XCTAssertTrue(cache2.isImageCachedForKey(URLString).cached, "This image should be cached in cache2.")
                 XCTAssertFalse(KingfisherManager.sharedManager.cache.isImageCachedForKey(URLString).cached, "This image should not be cached in default cache.")
                 XCTAssertFalse(KingfisherManager.sharedManager.cache.isImageCachedForKey(URLString).cached, "This image should not be cached in default cache.")