Jelajahi Sumber

Merge pull request #817 from onevcat/feature/image-modifier

Feature image modifier
Wei Wang 8 tahun lalu
induk
melakukan
662b5a6241

+ 6 - 4
Sources/ImageCache.swift

@@ -290,10 +290,11 @@ open class ImageCache {
         
         var block: RetrieveImageDiskTask?
         let options = options ?? KingfisherEmptyOptionsInfo
-        
+        let imageModifier = options.imageModifier
+
         if let image = self.retrieveImageInMemoryCache(forKey: key, options: options) {
             options.callbackDispatchQueue.safeAsync {
-                completionHandler(image, .memory)
+                completionHandler(imageModifier.modify(image), .memory)
             }
         } else if options.fromMemoryCacheOrRefresh { // Only allows to get images from memory cache.
             options.callbackDispatchQueue.safeAsync {
@@ -306,6 +307,7 @@ open class ImageCache {
                 if let image = sSelf.retrieveImageInDiskCache(forKey: key, options: options) {
                     if options.backgroundDecode {
                         sSelf.processQueue.async {
+
                             let result = image.kf.decoded
                             
                             sSelf.store(result,
@@ -315,7 +317,7 @@ open class ImageCache {
                                         toDisk: false,
                                         completionHandler: nil)
                             options.callbackDispatchQueue.safeAsync {
-                                completionHandler(result, .memory)
+                                completionHandler(imageModifier.modify(result), .memory)
                                 sSelf = nil
                             }
                         }
@@ -328,7 +330,7 @@ open class ImageCache {
                                     completionHandler: nil
                         )
                         options.callbackDispatchQueue.safeAsync {
-                            completionHandler(image, .disk)
+                            completionHandler(imageModifier.modify(image), .disk)
                             sSelf = nil
                         }
                     }

+ 7 - 5
Sources/ImageDownloader.swift

@@ -566,7 +566,6 @@ class ImageDownloaderSessionHandler: NSObject, URLSessionDataDelegate, Authentic
                 let callbackQueue = options.callbackDispatchQueue
                 
                 let processor = options.processor
-                
                 var image = imageCache[processor.identifier]
                 if let data = data, image == nil {
                     image = processor.process(item: .data(data), options: options)
@@ -576,14 +575,17 @@ class ImageDownloaderSessionHandler: NSObject, URLSessionDataDelegate, Authentic
                 }
                 
                 if let image = image {
-                    
+
                     downloader.delegate?.imageDownloader(downloader, didDownload: image, for: url, with: task.response)
-                    
+
+                    let imageModifier = options.imageModifier
+                    let finalImage = imageModifier.modify(image)
+
                     if options.backgroundDecode {
-                        let decodedImage = image.kf.decoded
+                        let decodedImage = finalImage.kf.decoded
                         callbackQueue.safeAsync { completionHandler?(decodedImage, nil, url, data) }
                     } else {
-                        callbackQueue.safeAsync { completionHandler?(image, nil, url, data) }
+                        callbackQueue.safeAsync { completionHandler?(finalImage, nil, url, data) }
                     }
                     
                 } else {

+ 79 - 71
Sources/ImageModifier.swift

@@ -38,7 +38,16 @@ public protocol ImageModifier {
     /// - Note: The return value will be unmodified if modifying is not possible on
     ///         the current platform.
     /// - Note: Most modifiers support UIImage or NSImage, but not CGImage.
-    func modify(image: Image) -> Image
+    func modify(_ image: Image) -> Image
+}
+
+extension ImageModifier {
+    func modify(_ image: Image?) -> Image? {
+        guard let image = image else {
+            return nil
+        }
+        return modify(image)
+    }
 }
 
 typealias ModifierImp = ((Image) -> Image)
@@ -46,7 +55,7 @@ typealias ModifierImp = ((Image) -> Image)
 fileprivate struct GeneralModifier: ImageModifier {
     let identifier: String
     let m: ModifierImp
-    func modify(image: Image) -> Image {
+    func modify(_ image: Image) -> Image {
         return m(image)
     }
 }
@@ -68,7 +77,7 @@ public struct DefaultImageModifier: ImageModifier {
     /// - returns: The modified image.
     ///
     /// - Note: See documentation of `ImageModifier` protocol for more.
-    public func modify(image: Image) -> Image {
+    public func modify(_ image: Image) -> Image {
         return image
     }
 }
@@ -93,7 +102,7 @@ public struct AnyImageModifier: ImageModifier {
     /// - returns: The modified image.
     ///
     /// - Note: See documentation of `ImageModifier` protocol for more.
-    public func modify(image: Image) -> Image {
+    public func modify(_ image: Image) -> Image {
         return block(image)
     }
 }
@@ -101,83 +110,82 @@ public struct AnyImageModifier: ImageModifier {
 #if os(iOS) || os(tvOS) || os(watchOS)
 import UIKit
 
-    /// Modifier for setting the rendering mode of images.
-    /// Only UI-based images are supported; if a non-UI image is passed in, the
-    /// modifier will do nothing.
-    public struct RenderingModeImageModifier: ImageModifier {
+/// Modifier for setting the rendering mode of images.
+/// Only UI-based images are supported; if a non-UI image is passed in, the
+/// modifier will do nothing.
+public struct RenderingModeImageModifier: ImageModifier {
 
-        /// The rendering mode to apply to the image.
-        public let renderingMode: UIImageRenderingMode
+    /// The rendering mode to apply to the image.
+    public let renderingMode: UIImageRenderingMode
 
-        /// Initialize a `RenderingModeImageModifier`
-        ///
-        /// - parameter renderingMode: The rendering mode to apply to the image.
-        ///                            Default is .automatic
-        public init(renderingMode: UIImageRenderingMode = .automatic) {
-            self.renderingMode = renderingMode
-        }
+    /// Initialize a `RenderingModeImageModifier`
+    ///
+    /// - parameter renderingMode: The rendering mode to apply to the image.
+    ///                            Default is .automatic
+    public init(renderingMode: UIImageRenderingMode = .automatic) {
+        self.renderingMode = renderingMode
+    }
 
-        /// Modify an input `Image`.
-        ///
-        /// - parameter image:   Image which will be modified by `self`
-        ///
-        /// - returns: The modified image.
-        ///
-        /// - Note: See documentation of `ImageModifier` protocol for more.
-        public func modify(image: Image) -> Image {
-            return image.withRenderingMode(renderingMode)
-        }
+    /// Modify an input `Image`.
+    ///
+    /// - parameter image:   Image which will be modified by `self`
+    ///
+    /// - returns: The modified image.
+    ///
+    /// - Note: See documentation of `ImageModifier` protocol for more.
+    public func modify(_ image: Image) -> Image {
+        return image.withRenderingMode(renderingMode)
     }
+}
+
+/// Modifier for setting the `flipsForRightToLeftLayoutDirection` property of images.
+/// Only UI-based images are supported; if a non-UI image is passed in, the
+/// modifier will do nothing.
+public struct FlipsForRightToLeftLayoutDirectionImageModifier: ImageModifier {
+    /// Initialize a `FlipsForRightToLeftLayoutDirectionImageModifier`
+    ///
+    /// - Note: On versions of iOS lower than 9.0, the image will be returned
+    ///         unmodified.
+    public init() {}
 
-    /// Modifier for setting the `flipsForRightToLeftLayoutDirection` property of images.
-    /// Only UI-based images are supported; if a non-UI image is passed in, the
-    /// modifier will do nothing.
-    public struct FlipsForRightToLeftLayoutDirectionImageModifier: ImageModifier {
-        /// Initialize a `FlipsForRightToLeftLayoutDirectionImageModifier`
-        ///
-        /// - Note: On versions of iOS lower than 9.0, the image will be returned
-        ///         unmodified.
-        public init() {}
-
-        /// Modify an input `Image`.
-        ///
-        /// - parameter image:   Image which will be modified by `self`
-        ///
-        /// - returns: The modified image.
-        ///
-        /// - Note: See documentation of `ImageModifier` protocol for more.
-        public func modify(image: Image) -> Image {
-            if #available(iOS 9.0, *) {
-                return image.imageFlippedForRightToLeftLayoutDirection()
-            } else {
-                return image
-            }
+    /// Modify an input `Image`.
+    ///
+    /// - parameter image:   Image which will be modified by `self`
+    ///
+    /// - returns: The modified image.
+    ///
+    /// - Note: See documentation of `ImageModifier` protocol for more.
+    public func modify(_ image: Image) -> Image {
+        if #available(iOS 9.0, *) {
+            return image.imageFlippedForRightToLeftLayoutDirection()
+        } else {
+            return image
         }
     }
+}
 
-    /// Modifier for setting the `alignmentRectInsets` property of images.
-    /// Only UI-based images are supported; if a non-UI image is passed in, the
-    /// modifier will do nothing.
-    public struct AlignmentRectInsetsImageModifier: ImageModifier {
+/// Modifier for setting the `alignmentRectInsets` property of images.
+/// Only UI-based images are supported; if a non-UI image is passed in, the
+/// modifier will do nothing.
+public struct AlignmentRectInsetsImageModifier: ImageModifier {
 
-        /// The alignment insets to apply to the image
-        public let alignmentInsets: UIEdgeInsets
+    /// The alignment insets to apply to the image
+    public let alignmentInsets: UIEdgeInsets
 
-        /// Initialize a `AlignmentRectInsetsImageModifier`
-        public init(alignmentInsets: UIEdgeInsets) {
-            self.alignmentInsets = alignmentInsets
-        }
-
-        /// Modify an input `Image`.
-        ///
-        /// - parameter image:   Image which will be modified by `self`
-        ///
-        /// - returns: The modified image.
-        ///
-        /// - Note: See documentation of `ImageModifier` protocol for more.
-        public func modify(image: Image) -> Image {
-            return image.withAlignmentRectInsets(alignmentInsets)
-        }
+    /// Initialize a `AlignmentRectInsetsImageModifier`
+    public init(alignmentInsets: UIEdgeInsets) {
+        self.alignmentInsets = alignmentInsets
     }
 
+    /// Modify an input `Image`.
+    ///
+    /// - parameter image:   Image which will be modified by `self`
+    ///
+    /// - returns: The modified image.
+    ///
+    /// - Note: See documentation of `ImageModifier` protocol for more.
+    public func modify(_ image: Image) -> Image {
+        return image.withAlignmentRectInsets(alignmentInsets)
+    }
+}
 #endif

+ 1 - 2
Sources/KingfisherManager.swift

@@ -198,8 +198,7 @@ public class KingfisherManager {
                               completionHandler: CompletionHandler?,
                                         options: KingfisherOptionsInfo)
     {
-        
-        
+
         let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in
             completionHandler?(image, error, cacheType, imageURL)
         }

+ 49 - 5
Tests/KingfisherTests/ImageCacheTests.swift

@@ -76,7 +76,7 @@ class ImageCacheTests: XCTestCase {
         let expectation = self.expectation(description: "wait for clearing disk cache")
         let key = testKeys[0]
         
-        cache.store(testImage, original: testImageData as Data?, forKey: key, toDisk: true) { () -> () in
+        cache.store(testImage, original: testImageData as Data, forKey: key, toDisk: true) {
             self.cache.clearMemoryCache()
             let cacheResult = self.cache.imageCachedType(forKey: key)
             XCTAssertTrue(cacheResult.cached, "Should be cached")
@@ -94,7 +94,7 @@ class ImageCacheTests: XCTestCase {
     func testClearMemoryCache() {
         let expectation = self.expectation(description: "wait for retrieving image")
         
-        cache.store(testImage, original: testImageData as Data?, forKey: testKeys[0], toDisk: true) { () -> () in
+        cache.store(testImage, original: testImageData as Data, forKey: testKeys[0], toDisk: true) { () -> () in
             self.cache.clearMemoryCache()
             self.cache.retrieveImage(forKey: testKeys[0], options: nil, completionHandler: { (image, type) -> () in
                 XCTAssert(image != nil && type == .disk, "Should be cached in disk. But \(type)")
@@ -263,7 +263,7 @@ class ImageCacheTests: XCTestCase {
         let expectation = self.expectation(description: "wait for caching image")
         
         XCTAssert(self.cache.imageCachedType(forKey: testKeys[0]).cached == false, "This image should not be cached yet.")
-        self.cache.store(testImage, original: testImageData as Data?, forKey: testKeys[0], toDisk: true) { () -> () in
+        self.cache.store(testImage, original: testImageData as Data, forKey: testKeys[0], toDisk: true) { () -> () in
             XCTAssert(self.cache.imageCachedType(forKey: testKeys[0]).cached == true, "This image should be already cached.")
             expectation.fulfill()
         }
@@ -274,7 +274,7 @@ class ImageCacheTests: XCTestCase {
     func testRetrievingImagePerformance() {
 
         let expectation = self.expectation(description: "wait for retrieving image")
-        self.cache.store(testImage, original: testImageData as Data?, forKey: testKeys[0], toDisk: true) { () -> () in
+        self.cache.store(testImage, original: testImageData as Data, forKey: testKeys[0], toDisk: true) { () -> () in
             self.measure({ () -> Void in
                 for _ in 1 ..< 200 {
                     _ = self.cache.retrieveImageInDiskCache(forKey: testKeys[0])
@@ -350,7 +350,51 @@ class ImageCacheTests: XCTestCase {
         
         waitForExpectations(timeout: 5, handler: nil)
     }
-    
+
+#if os(iOS) || os(tvOS) || os(watchOS)
+    func testGettingMemoryCachedImageCouldBeModified() {
+
+        let expectation = self.expectation(description: "wait for retrieving image")
+
+        var modifierCalled = false
+        let modifier = AnyImageModifier { image in
+            modifierCalled = true
+            return image.withRenderingMode(.alwaysTemplate)
+        }
+
+        cache.store(testImage, original: testImageData as Data?, forKey: testKeys[0]) {
+            self.cache.retrieveImage(forKey: testKeys[0], options: [.imageModifier(modifier)]) {
+                image, _ in
+                XCTAssertTrue(modifierCalled)
+                XCTAssertEqual(image?.renderingMode, .alwaysTemplate)
+                expectation.fulfill()
+            }
+        }
+        waitForExpectations(timeout: 5, handler: nil)
+    }
+
+    func testGettingDiskCachedImageCouldBeModified() {
+        let expectation = self.expectation(description: "wait for retrieving image")
+
+        var modifierCalled = false
+        let modifier = AnyImageModifier { image in
+            modifierCalled = true
+            return image.withRenderingMode(.alwaysTemplate)
+        }
+
+        cache.store(testImage, original: testImageData as Data?, forKey: testKeys[0]) {
+            self.cache.clearMemoryCache()
+            self.cache.retrieveImage(forKey: testKeys[0], options: [.imageModifier(modifier)]) {
+                image, _ in
+                XCTAssertTrue(modifierCalled)
+                XCTAssertEqual(image?.renderingMode, .alwaysTemplate)
+                expectation.fulfill()
+            }
+        }
+        waitForExpectations(timeout: 5, handler: nil)
+    }
+#endif
+
     // MARK: - Helper
     func storeMultipleImages(_ completionHandler:@escaping ()->()) {
         

+ 26 - 0
Tests/KingfisherTests/ImageDownloaderTests.swift

@@ -400,6 +400,32 @@ class ImageDownloaderTests: XCTestCase {
         }
         waitForExpectations(timeout: 5, handler: nil)
     }
+
+#if os(iOS) || os(tvOS) || os(watchOS)
+    func testDownloadedImageCouldBeModified() {
+        let expectation = self.expectation(description: "wait for downloading image")
+
+        let URLString = testKeys[0]
+        _ = stubRequest("GET", URLString).andReturn(200)?.withBody(testImageData)
+
+        let url = URL(string: URLString)!
+
+        var modifierCalled = false
+        let modifier = AnyImageModifier { image in
+            modifierCalled = true
+            return image.withRenderingMode(.alwaysTemplate)
+        }
+
+        downloader.downloadImage(with: url, options: [.imageModifier(modifier)]) {
+            image, _, _, _ in
+            XCTAssertTrue(modifierCalled)
+            XCTAssertEqual(image?.renderingMode, .alwaysTemplate)
+            expectation.fulfill()
+        }
+
+        waitForExpectations(timeout: 5, handler: nil)
+    }
+#endif
 }
 
 extension ImageDownloaderTests: ImageDownloaderDelegate {

+ 5 - 5
Tests/KingfisherTests/ImageModifierTests.swift

@@ -44,7 +44,7 @@ class ImageModifierTests: XCTestCase {
             return image
         })
         let image = Image(data: testImagePNGData)!
-        let modifiedImage = m.modify(image: image)
+        let modifiedImage = m.modify(image)
         XCTAssert(modifiedImage == image)
     }
 
@@ -53,18 +53,18 @@ class ImageModifierTests: XCTestCase {
     func testRenderingModeImageModifier() {
         let m1 = RenderingModeImageModifier(renderingMode: .alwaysOriginal)
         let image = Image(data: testImagePNGData)!
-        let alwaysOriginalImage = m1.modify(image: image)
+        let alwaysOriginalImage = m1.modify(image)
         XCTAssert(alwaysOriginalImage.renderingMode == .alwaysOriginal)
 
         let m2 = RenderingModeImageModifier(renderingMode: .alwaysTemplate)
-        let alwaysTemplateImage = m2.modify(image: image)
+        let alwaysTemplateImage = m2.modify(image)
         XCTAssert(alwaysTemplateImage.renderingMode == .alwaysTemplate)
     }
 
     func testFlipsForRightToLeftLayoutDirectionImageModifier() {
         let m = FlipsForRightToLeftLayoutDirectionImageModifier()
         let image = Image(data: testImagePNGData)!
-        let modifiedImage = m.modify(image: image)
+        let modifiedImage = m.modify(image)
         if #available(iOS 9.0, *) {
             XCTAssert(modifiedImage.flipsForRightToLeftLayoutDirection == true)
         } else {
@@ -76,7 +76,7 @@ class ImageModifierTests: XCTestCase {
         let insets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
         let m = AlignmentRectInsetsImageModifier(alignmentInsets: insets)
         let image = Image(data: testImagePNGData)!
-        let modifiedImage = m.modify(image: image)
+        let modifiedImage = m.modify(image)
         XCTAssert(modifiedImage.alignmentRectInsets == insets)
     }
 

+ 1 - 1
Tests/KingfisherTests/ImageProcessorTests.swift

@@ -49,7 +49,7 @@ class ImageProcessorTests: XCTestCase {
     }
     
     func testRenderEqual() {
-        let image1 = Image(data: testImageData! as Data)!
+        let image1 = Image(data: testImageData as Data)!
         let image2 = Image(data: testImagePNGData)!
         
         XCTAssertTrue(image1.renderEqual(to: image2))

+ 1 - 1
Tests/KingfisherTests/ImageViewExtensionTests.swift

@@ -392,7 +392,7 @@ class ImageViewExtensionTests: XCTestCase {
     
     func testCanUseImageIndicatorViewAnimating() {
         
-        imageView.kf.indicatorType = .image(imageData: testImageData! as Data)
+        imageView.kf.indicatorType = .image(imageData: testImageData as Data)
         XCTAssertTrue(imageView.kf.indicator is ImageIndicator)
         let image = (imageView.kf.indicator?.view as? ImageView)?.image
         XCTAssertNotNil(image)

+ 71 - 2
Tests/KingfisherTests/KingfisherManagerTests.swift

@@ -366,7 +366,7 @@ class KingfisherManagerTests: XCTestCase {
         let expectation = self.expectation(description: "waiting for downloading finished")
         
         let URLString = testKeys[0]
-        manager.cache.store(testImage, original: testImageData! as Data,
+        manager.cache.store(testImage, original: testImageData as Data,
                             forKey: URLString, processorIdentifier: DefaultImageProcessor.default.identifier,
                             cacheSerializer: DefaultCacheSerializer.default, toDisk: true)
         {
@@ -431,7 +431,7 @@ class KingfisherManagerTests: XCTestCase {
         // Clear original cache first.
         originalCache.clearMemoryCache()
         originalCache.clearDiskCache {
-            originalCache.store(testImage, original: testImageData! as Data,
+            originalCache.store(testImage, original: testImageData as Data,
                                 forKey: URLString, processorIdentifier: DefaultImageProcessor.default.identifier,
                                 cacheSerializer: DefaultCacheSerializer.default, toDisk: true)
             {
@@ -548,6 +548,75 @@ class KingfisherManagerTests: XCTestCase {
         }
         waitForExpectations(timeout: 5, handler: nil)
     }
+
+#if os(iOS) || os(tvOS) || os(watchOS)
+    func testShouldApplyImageModifierWhenDownload() {
+        let expectation = self.expectation(description: "waiting for downloading and cache")
+
+        let URLString = testKeys[0]
+        _ = stubRequest("GET", URLString).andReturn(200)?.withBody(testImageData)
+        let url = URL(string: URLString)!
+
+        var modifierCalled = false
+        let modifier = AnyImageModifier { image in
+            modifierCalled = true
+            return image.withRenderingMode(.alwaysTemplate)
+        }
+        manager.retrieveImage(with: url, options: [.imageModifier(modifier)], progressBlock: nil) {
+            image, _, _, _ in
+            XCTAssertTrue(modifierCalled)
+            XCTAssertEqual(image?.renderingMode, .alwaysTemplate)
+            expectation.fulfill()
+        }
+        waitForExpectations(timeout: 5, handler: nil)
+    }
+
+    func testShouldApplyImageModifierWhenLoadFromMemoryCache() {
+        let expectation = self.expectation(description: "waiting for downloading and cache")
+        let URLString = testKeys[0]
+        let url = URL(string: URLString)!
+
+        var modifierCalled = false
+        let modifier = AnyImageModifier { image in
+            modifierCalled = true
+            return image.withRenderingMode(.alwaysTemplate)
+        }
+
+        manager.cache.store(testImage, forKey: URLString)
+        manager.retrieveImage(with: url, options: [.imageModifier(modifier)], progressBlock: nil) {
+            image, _, type, _ in
+            XCTAssertTrue(modifierCalled)
+            XCTAssertEqual(type, .memory)
+            XCTAssertEqual(image?.renderingMode, .alwaysTemplate)
+            expectation.fulfill()
+        }
+        waitForExpectations(timeout: 5, handler: nil)
+    }
+
+    func testShouldApplyImageModifierWhenLoadFromDiskCache() {
+        let expectation = self.expectation(description: "waiting for downloading and cache")
+        let URLString = testKeys[0]
+        let url = URL(string: URLString)!
+
+        var modifierCalled = false
+        let modifier = AnyImageModifier { image in
+            modifierCalled = true
+            return image.withRenderingMode(.alwaysTemplate)
+        }
+
+        manager.cache.store(testImage, forKey: URLString) {
+            self.manager.cache.clearMemoryCache()
+            self.manager.retrieveImage(with: url, options: [.imageModifier(modifier)], progressBlock: nil) {
+                image, _, type, _ in
+                XCTAssertTrue(modifierCalled)
+                XCTAssertEqual(type, .disk)
+                XCTAssertEqual(image?.renderingMode, .alwaysTemplate)
+                expectation.fulfill()
+            }
+        }
+        waitForExpectations(timeout: 5, handler: nil)
+    }
+#endif
 }
 
 class SimpleProcessor: ImageProcessor {

File diff ditekan karena terlalu besar
+ 0 - 0
Tests/KingfisherTests/KingfisherTestHelper.swift


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini