Преглед изворни кода

Merge pull request #1 from onevcat/lixiang1994-master

Conceptual proving of side effect option
LEE пре 6 година
родитељ
комит
2a1804e055

+ 0 - 5
Demo/Demo/Kingfisher-Demo/Info.plist

@@ -22,11 +22,6 @@
 	<string>1244</string>
 	<key>LSRequiresIPhoneOS</key>
 	<true/>
-	<key>NSAppTransportSecurity</key>
-	<dict>
-		<key>NSAllowsArbitraryLoads</key>
-		<true/>
-	</dict>
 	<key>UILaunchStoryboardName</key>
 	<string>LaunchScreen</string>
 	<key>UIMainStoryboardFile</key>

+ 11 - 6
Demo/Demo/Kingfisher-Demo/Resources/ImageLoader.swift

@@ -28,17 +28,22 @@ import Foundation
 
 struct ImageLoader {
     static let sampleImageURLs: [URL] = {
-        let prefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/"
-        return (1...10).map { URL(string: "\(prefix)kingfisher-\($0).jpg")! }
+        let prefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading"
+        return (1...10).map { URL(string: "\(prefix)/kingfisher-\($0).jpg")! }
     }()
 
     static let highResolutionImageURLs: [URL] = {
-        let prefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/HighResolution/"
-        return (1...20).map { URL(string: "\(prefix)\($0).jpg")! }
+        let prefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/HighResolution"
+        return (1...20).map { URL(string: "\(prefix)/\($0).jpg")! }
     }()
     
     static let gifImageURLs: [URL] = {
-        let prefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF/"
-        return (1...3).map { URL(string: "\(prefix)\($0).gif")! }
+        let prefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF"
+        return (1...3).map { URL(string: "\(prefix)/\($0).gif")! }
+    }()
+
+    static let progressiveImageURL: URL = {
+        let prefix = "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Progressive"
+        return URL(string: "\(prefix)/progressive.jpg")!
     }()
 }

+ 5 - 7
Demo/Demo/Kingfisher-Demo/ViewControllers/ProgressiveJPEGViewController.swift

@@ -36,8 +36,6 @@ class ProgressiveJPEGViewController: UIViewController {
     private var isWait = true
     private var isFastestScan = true
     
-    private let url = URL(string: "https://demo-resources.oss-cn-beijing.aliyuncs.com/progressive.jpg")!
-//    private let url = URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/Loading/kingfisher-9.jpg")!
     private let processor = RoundCornerImageProcessor(cornerRadius: 30)
     
     override func viewDidLoad() {
@@ -58,7 +56,7 @@ class ProgressiveJPEGViewController: UIViewController {
         )
         
         imageView.kf.setImage(
-            with: url,
+            with: ImageLoader.progressiveImageURL,
             placeholder: nil,
             options: [.loadDiskFileSynchronously,
                       .progressiveJPEG(progressive),
@@ -82,7 +80,7 @@ class ProgressiveJPEGViewController: UIViewController {
             imageView.kf.cancelDownloadTask()
             // Clean cache
             KingfisherManager.shared.cache.removeImage(
-                forKey: self.url.cacheKey,
+                forKey: ImageLoader.progressiveImageURL.cacheKey,
                 processorIdentifier: self.processor.identifier,
                 callbackQueue: .mainAsync,
                 completionHandler: {
@@ -92,7 +90,7 @@ class ProgressiveJPEGViewController: UIViewController {
         }
         
         do {
-            let title = isBlur ? "Close Blur" : "Enabled Blur"
+            let title = isBlur ? "Disable Blur" : "Enable Blur"
             alert.addAction(UIAlertAction(title: title, style: .default) { _ in
                 self.isBlur.toggle()
                 reloadImage()
@@ -100,7 +98,7 @@ class ProgressiveJPEGViewController: UIViewController {
         }
         
         do {
-            let title = isWait ? "Close Wait" : "Enabled Wait"
+            let title = isWait ? "Disable Wait" : "Enable Wait"
             alert.addAction(UIAlertAction(title: title, style: .default) { _ in
                 self.isWait.toggle()
                 reloadImage()
@@ -108,7 +106,7 @@ class ProgressiveJPEGViewController: UIViewController {
         }
         
         do {
-            let title = isFastestScan ? "Close Fastest Scan" : "Enabled Fastest Scan"
+            let title = isFastestScan ? "Disable Fastest Scan" : "Enable Fastest Scan"
             alert.addAction(UIAlertAction(title: title, style: .default) { _ in
                 self.isFastestScan.toggle()
                 reloadImage()

+ 3 - 3
Demo/Kingfisher-Demo.xcodeproj/project.pbxproj

@@ -497,7 +497,7 @@
 					};
 					D1ED2D0A1AD2CFA600CFC3EB = {
 						CreatedOnToolsVersion = 6.2;
-						DevelopmentTeam = 683UGRW72Z;
+						DevelopmentTeam = T499X543T7;
 						LastSwiftMigration = 0900;
 					};
 				};
@@ -966,7 +966,7 @@
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CODE_SIGN_IDENTITY = "iPhone Developer";
-				DEVELOPMENT_TEAM = 683UGRW72Z;
+				DEVELOPMENT_TEAM = T499X543T7;
 				INFOPLIST_FILE = "Demo/Kingfisher-Demo/Info.plist";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.$(PRODUCT_NAME:rfc1034identifier)";
@@ -979,7 +979,7 @@
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CODE_SIGN_IDENTITY = "iPhone Developer";
-				DEVELOPMENT_TEAM = 683UGRW72Z;
+				DEVELOPMENT_TEAM = T499X543T7;
 				INFOPLIST_FILE = "Demo/Kingfisher-Demo/Info.plist";
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = "com.onevcat.$(PRODUCT_NAME:rfc1034identifier)";

+ 5 - 2
Kingfisher.xcodeproj/project.pbxproj

@@ -41,6 +41,7 @@
 		4B8E291D216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8E291B216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift */; };
 		4B8E291E216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8E291B216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift */; };
 		4B8E291F216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8E291B216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift */; };
+		4BA3BF1E228BCDD100909201 /* DataReceivingSideEffectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BA3BF1D228BCDD100909201 /* DataReceivingSideEffectTests.swift */; };
 		4BCFF7A621990DB70055AAC4 /* MemoryStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCFF7A521990DB60055AAC4 /* MemoryStorageTests.swift */; };
 		4BCFF7A721990DB70055AAC4 /* MemoryStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCFF7A521990DB60055AAC4 /* MemoryStorageTests.swift */; };
 		4BCFF7A821990DB70055AAC4 /* MemoryStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCFF7A521990DB60055AAC4 /* MemoryStorageTests.swift */; };
@@ -295,6 +296,7 @@
 		4B8351CB217084660081EED8 /* Runtime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Runtime.swift; sourceTree = "<group>"; };
 		4B8E2916216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloaderDelegate.swift; sourceTree = "<group>"; };
 		4B8E291B216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationChallengeResponsable.swift; sourceTree = "<group>"; };
+		4BA3BF1D228BCDD100909201 /* DataReceivingSideEffectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataReceivingSideEffectTests.swift; sourceTree = "<group>"; };
 		4BCCF3441D5B0457003387C2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		4BCFF7A521990DB60055AAC4 /* MemoryStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryStorageTests.swift; sourceTree = "<group>"; };
 		4BCFF7A9219932390055AAC4 /* DiskStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskStorageTests.swift; sourceTree = "<group>"; };
@@ -608,6 +610,7 @@
 				D1E564402199C21E0057AAE3 /* StorageExpirationTests.swift */,
 				D1A1CC9E21A0F98600263AD8 /* ImageDataProviderTests.swift */,
 				D1BFED94222ACC6B009330C8 /* ImageProcessorTests.swift */,
+				4BA3BF1D228BCDD100909201 /* DataReceivingSideEffectTests.swift */,
 			);
 			name = KingfisherTests;
 			path = Tests/KingfisherTests;
@@ -882,10 +885,9 @@
 			};
 			buildConfigurationList = D1ED2D061AD2CFA600CFC3EB /* Build configuration list for PBXProject "Kingfisher" */;
 			compatibilityVersion = "Xcode 3.2";
-			developmentRegion = English;
+			developmentRegion = en;
 			hasScannedForEncodings = 0;
 			knownRegions = (
-				English,
 				en,
 				Base,
 			);
@@ -1340,6 +1342,7 @@
 				D1DC4B411D60996D00DFDFAA /* StringExtensionTests.swift in Sources */,
 				D1BFED95222ACC6B009330C8 /* ImageProcessorTests.swift in Sources */,
 				D12E0C501C47F23500AC98AD /* ImageCacheTests.swift in Sources */,
+				4BA3BF1E228BCDD100909201 /* DataReceivingSideEffectTests.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 57 - 59
Sources/Extensions/ImageView+Kingfisher.swift

@@ -31,6 +31,8 @@ import AppKit
 import UIKit
 #endif
 
+extension ImageView: ImageSettable {}
+
 extension KingfisherWrapper where Base: ImageView {
 
     // MARK: Setting Image
@@ -104,74 +106,70 @@ extension KingfisherWrapper where Base: ImageView {
         if base.shouldPreloadAllAnimation() {
             options.preloadAllAnimationData = true
         }
-        
-        let progressive = ImageProgressiveProvider(
-            options,
-            isContinue: { () -> Bool in
-                issuedIdentifier == self.taskIdentifier
-            },
-            refreshImage: { (image) in
-                self.base.image = image
-            }
-        )
-        
+
+        if let progressBlock = progressBlock {
+            options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(progressBlock)]
+        }
+
+        if let progressive = options.progressiveJPEG {
+
+            let p = ImageProgressiveProvider(
+                options,
+                isContinue: { () -> Bool in
+                    issuedIdentifier == self.taskIdentifier
+                },
+                refreshImage: { (image) in
+                    //self.base.image = image
+                }
+            )!
+
+            p.imageSettable = base
+
+            options.onDataReceived = (options.onDataReceived ?? []) + [p]
+        }
+
+        options.onDataReceived?.forEach {
+            $0.onShouldApply = { issuedIdentifier == self.taskIdentifier }
+        }
+
         let task = KingfisherManager.shared.retrieveImage(
             with: source,
             options: options,
-            receivedBlock: { latest, received in
-                guard issuedIdentifier == self.taskIdentifier else { return }
-                let callbacks = mutatingSelf.imageTask?.sessionTask.callbacks ?? []
-                progressive?.update(data: received, with: callbacks)
-            },
-            progressBlock: { receivedSize, totalSize in
-                guard issuedIdentifier == self.taskIdentifier else { return }
-                progressBlock?(receivedSize, totalSize)
-            },
             completionHandler: { result in
                 CallbackQueue.mainCurrentOrAsync.execute {
-                    func handler() {
-                        maybeIndicator?.stopAnimatingView()
-                        guard issuedIdentifier == self.taskIdentifier else {
-                            let reason: KingfisherError.ImageSettingErrorReason
-                            do {
-                                let value = try result.get()
-                                reason = .notCurrentSourceTask(result: value, error: nil, source: source)
-                            } catch {
-                                reason = .notCurrentSourceTask(result: nil, error: error, source: source)
-                            }
-                            let error = KingfisherError.imageSettingError(reason: reason)
-                            completionHandler?(.failure(error))
+                    maybeIndicator?.stopAnimatingView()
+                    guard issuedIdentifier == self.taskIdentifier else {
+                        let reason: KingfisherError.ImageSettingErrorReason
+                        do {
+                            let value = try result.get()
+                            reason = .notCurrentSourceTask(result: value, error: nil, source: source)
+                        } catch {
+                            reason = .notCurrentSourceTask(result: nil, error: error, source: source)
+                        }
+                        let error = KingfisherError.imageSettingError(reason: reason)
+                        completionHandler?(.failure(error))
+                        return
+                    }
+
+                    mutatingSelf.imageTask = nil
+
+                    switch result {
+                    case .success(let value):
+                        guard self.needsTransition(options: options, cacheType: value.cacheType) else {
+                            mutatingSelf.placeholder = nil
+                            self.base.image = value.image
+                            completionHandler?(result)
                             return
                         }
-                        
-                        mutatingSelf.imageTask = nil
-                        
-                        switch result {
-                        case .success(let value):
-                            guard self.needsTransition(options: options, cacheType: value.cacheType) else {
-                                mutatingSelf.placeholder = nil
-                                self.base.image = value.image
-                                completionHandler?(result)
-                                return
-                            }
-                            
-                            self.makeTransition(image: value.image, transition: options.transition) {
-                                completionHandler?(result)
-                            }
-                            
-                        case .failure:
-                            if let image = options.onFailureImage {
-                                self.base.image = image
-                            }
+
+                        self.makeTransition(image: value.image, transition: options.transition) {
                             completionHandler?(result)
                         }
-                    }
-                    
-                    if let progressive = progressive {
-                        progressive.finished { handler() }
-                        
-                    } else {
-                        handler()
+                    case .failure:
+                        if let image = options.onFailureImage {
+                            self.base.image = image
+                        }
+                        completionHandler?(result)
                     }
                 }
             }

+ 39 - 1
Sources/General/KingfisherOptionsInfo.swift

@@ -216,6 +216,8 @@ public enum KingfisherOptionsInfoItem {
     
     /// Enable progressive image loading, Kingfisher will use the `ImageProgressive` of
     case progressiveJPEG(ImageProgressive)
+
+    case onDataReceived([DataReceivingSideEffect])
 }
 
 // Improve performance by parsing the input `KingfisherOptionsInfo` (self) first.
@@ -254,7 +256,9 @@ public struct KingfisherParsedOptionsInfo {
     public var memoryCacheExpiration: StorageExpiration? = nil
     public var diskCacheExpiration: StorageExpiration? = nil
     public var processingQueue: CallbackQueue? = nil
-    public var progressiveJPEG: ImageProgressive?
+    public var progressiveJPEG: ImageProgressive? = nil
+
+    public var onDataReceived: [DataReceivingSideEffect]? = nil
     
     public init(_ info: KingfisherOptionsInfo?) {
         guard let info = info else { return }
@@ -291,6 +295,7 @@ public struct KingfisherParsedOptionsInfo {
             case .diskCacheExpiration(let expiration): diskCacheExpiration = expiration
             case .processingQueue(let queue): processingQueue = queue
             case .progressiveJPEG(let value): progressiveJPEG = value
+            case .onDataReceived(let value): onDataReceived = value
             }
         }
 
@@ -309,3 +314,36 @@ extension KingfisherParsedOptionsInfo {
             onlyFirstFrame: onlyLoadFirstFrame)
     }
 }
+
+public protocol DataReceivingSideEffect: AnyObject {
+    func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data)
+    var onShouldApply: () -> Bool { get set }
+}
+
+class ImageLoadingProgressSideEffect: DataReceivingSideEffect {
+
+    var onShouldApply: () -> Bool = { return true }
+
+
+    let block: DownloadProgressBlock
+
+    init(_ block: @escaping DownloadProgressBlock) {
+        self.block = block
+    }
+
+    func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) {
+
+        guard onShouldApply() else { return }
+
+        guard let expectedContentLength = task.task.response?.expectedContentLength,
+                  expectedContentLength != -1 else
+        {
+            return
+        }
+
+        let dataLength = Int64(task.mutableData.count)
+        DispatchQueue.main.async {
+            self.block(dataLength, expectedContentLength)
+        }
+    }
+}

+ 29 - 21
Sources/Image/ImageProgressive.swift

@@ -60,8 +60,20 @@ public struct ImageProgressive {
     }
 }
 
-final class ImageProgressiveProvider {
-    
+protocol ImageSettable: AnyObject {
+    var image: Image? { get set }
+}
+
+final class ImageProgressiveProvider: DataReceivingSideEffect {
+
+    weak var imageSettable: ImageSettable?
+
+    func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) {
+        update(data: task.mutableData, with: task.callbacks)
+    }
+
+    var onShouldApply: () -> Bool = { return true }
+
     private let options: KingfisherParsedOptionsInfo
     private let refreshClosure: (Image) -> Void
     private let isContinueClosure: () -> Bool
@@ -91,31 +103,27 @@ final class ImageProgressiveProvider {
         let isFastest = option.isFastestScan
         
         func add(decode data: Data) {
-            queue.add(minimum: interval) { (completion) in
+            queue.add(minimum: interval) { completion in
                 guard self.isContinueClosure() else {
                     completion()
                     return
                 }
-                
-                self.decoder.decode(data, with: callbacks) { (image) in
+                self.decoder.decode(data, with: callbacks) { image in
                     defer { completion() }
-                    guard self.isContinueClosure() else { return }
-                    guard self.isWait || !self.isFinished else { return }
+                    // guard self.isContinueClosure() else { return }
+                    // guard self.isWait || !self.isFinishedClosure() else { return }
                     guard let image = image else { return }
-                    
-                    self.refreshClosure(image)
+                    self.imageSettable?.image = image
                 }
             }
         }
         
         if isFastest {
-            guard let data: Data = decoder.scanning(data) else { return }
-            
+            guard let data = decoder.scanning(data) else { return }
             add(decode: data)
-            
         } else {
-            let datas: [Data] = decoder.scanning(data)
-            for data in datas {
+            let allData: [Data] = decoder.scanning(data)
+            for data in allData {
                 add(decode: data)
             }
         }
@@ -138,10 +146,10 @@ final class ImageProgressiveProvider {
     }
 }
 
-fileprivate final class ImageProgressiveDecoder {
+private final class ImageProgressiveDecoder {
     
     private let options: KingfisherParsedOptionsInfo
-    private(set) var scannedCount: Int = 0
+    private(set) var scannedCount = 0
     private var scannedIndex = -1
     
     init(_ options: KingfisherParsedOptionsInfo) {
@@ -152,7 +160,7 @@ fileprivate final class ImageProgressiveDecoder {
         guard data.kf.contains(jpeg: .SOF2) else {
             return []
         }
-        guard (scannedIndex + 1) < data.count else {
+        guard scannedIndex + 1 < data.count else {
             return []
         }
         
@@ -160,7 +168,7 @@ fileprivate final class ImageProgressiveDecoder {
         var index = scannedIndex + 1
         var count = scannedCount
         
-        while index < (data.count - 1) {
+        while index < data.count - 1 {
             scannedIndex = index
             // 0xFF, 0xDA - Start Of Scan
             let SOS = ImageFormat.JPEGMarker.SOS.bytes
@@ -188,7 +196,7 @@ fileprivate final class ImageProgressiveDecoder {
         guard data.kf.contains(jpeg: .SOF2) else {
             return nil
         }
-        guard (scannedIndex + 1) < data.count else {
+        guard scannedIndex + 1 < data.count else {
             return nil
         }
         
@@ -196,7 +204,7 @@ fileprivate final class ImageProgressiveDecoder {
         var count = scannedCount
         var lastSOSIndex = 0
         
-        while index < (data.count - 1) {
+        while index < data.count - 1 {
             scannedIndex = index
             // 0xFF, 0xDA - Start Of Scan
             let SOS = ImageFormat.JPEGMarker.SOS.bytes
@@ -267,7 +275,7 @@ fileprivate final class ImageProgressiveDecoder {
     }
 }
 
-fileprivate final class ImageProgressiveSerialQueue {
+private final class ImageProgressiveSerialQueue {
     typealias ClosureCallback = ((@escaping () -> Void)) -> Void
     
     private let queue: DispatchQueue

+ 4 - 5
Sources/Networking/SessionDelegate.swift

@@ -175,11 +175,10 @@ extension SessionDelegate: URLSessionDataDelegate {
                 }
             }
         }
-        
-        let received = task.mutableData
-        DispatchQueue.main.async {
-            task.callbacks.forEach { callback in
-                callback.onReceived?.call((data, received))
+
+        task.callbacks.forEach { callback in
+            callback.options.onDataReceived?.forEach { sideEffect in
+                sideEffect.onDataReceived(session, task: task, data: data)
             }
         }
     }

+ 117 - 0
Tests/KingfisherTests/DataReceivingSideEffectTests.swift

@@ -0,0 +1,117 @@
+//
+//  DataReceivingSideEffectTests.swift
+//  Kingfisher
+//
+//  Created by jp20028 on 2019/05/15.
+//
+//  Copyright (c) 2019 Wei Wang <onevcat@gmail.com>
+//
+//  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.
+
+import XCTest
+@testable import Kingfisher
+
+class DataReceivingSideEffectTests: XCTestCase {
+
+    var manager: KingfisherManager!
+
+    override class func setUp() {
+        super.setUp()
+        LSNocilla.sharedInstance().start()
+    }
+
+    override class func tearDown() {
+        LSNocilla.sharedInstance().stop()
+        super.tearDown()
+    }
+
+    override func setUp() {
+        super.setUp()
+        // Put setup code here. This method is called before the invocation of each test method in the class.
+        let uuid = UUID()
+        let downloader = ImageDownloader(name: "test.manager.\(uuid.uuidString)")
+        let cache = ImageCache(name: "test.cache.\(uuid.uuidString)")
+
+        manager = KingfisherManager(downloader: downloader, cache: cache)
+    }
+
+    override func tearDown() {
+        LSNocilla.sharedInstance().clearStubs()
+        clearCaches([manager.cache])
+        cleanDefaultCache()
+        manager = nil
+        super.tearDown()
+    }
+
+    func testDataReceivingSideEffectBlockCanBeCalled() {
+        let exp = expectation(description: #function)
+        let url = testURLs[0]
+        stub(url, data: testImageData, length: 123)
+
+        let receiver = DataReceivingStub()
+
+        let options: KingfisherOptionsInfo = [.onDataReceived([receiver]), .waitForCache]
+        KingfisherManager.shared.retrieveImage(with: url, options: options) {
+            result in
+            XCTAssertTrue(receiver.called)
+            exp.fulfill()
+        }
+        waitForExpectations(timeout: 3, handler: nil)
+    }
+
+    func testDataReceivingSideEffectBlockCanBeCalledButNotApply() {
+        let exp = expectation(description: #function)
+        let url = testURLs[0]
+        stub(url, data: testImageData, length: 123)
+
+        let receiver = DataReceivingNotAppyStub()
+
+        let options: KingfisherOptionsInfo = [.onDataReceived([receiver]), .waitForCache]
+        KingfisherManager.shared.retrieveImage(with: url, options: options) {
+            result in
+            XCTAssertTrue(receiver.called)
+            XCTAssertFalse(receiver.appied)
+            exp.fulfill()
+        }
+        waitForExpectations(timeout: 3, handler: nil)
+    }
+}
+
+class DataReceivingStub: DataReceivingSideEffect {
+    var called: Bool = false
+    var onShouldApply: () -> Bool = { return true }
+    func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) {
+        self.called = true
+    }
+}
+
+class DataReceivingNotAppyStub: DataReceivingSideEffect {
+
+    var called: Bool = false
+    var appied: Bool = false
+
+    var onShouldApply: () -> Bool = { return false }
+
+    func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) {
+        called = true
+        if onShouldApply() {
+            appied = true
+        }
+    }
+}