Browse Source

Split processor, delegate and task files

onevcat 7 years ago
parent
commit
4b1c0fd587

+ 20 - 0
Kingfisher.xcodeproj/project.pbxproj

@@ -41,6 +41,14 @@
 		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 */; };
+		4BD821622189FC0C0084CC21 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD821612189FC0C0084CC21 /* SessionDelegate.swift */; };
+		4BD821632189FC0C0084CC21 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD821612189FC0C0084CC21 /* SessionDelegate.swift */; };
+		4BD821642189FC0C0084CC21 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD821612189FC0C0084CC21 /* SessionDelegate.swift */; };
+		4BD821652189FC0C0084CC21 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD821612189FC0C0084CC21 /* SessionDelegate.swift */; };
+		4BD821672189FD330084CC21 /* SessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD821662189FD330084CC21 /* SessionDataTask.swift */; };
+		4BD821682189FD330084CC21 /* SessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD821662189FD330084CC21 /* SessionDataTask.swift */; };
+		4BD821692189FD330084CC21 /* SessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD821662189FD330084CC21 /* SessionDataTask.swift */; };
+		4BD8216A2189FD330084CC21 /* SessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD821662189FD330084CC21 /* SessionDataTask.swift */; };
 		AE1D6776DC6183B84B561961 /* libPods-KingfisherTests-tvOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 124FABC032484C46AC221D0C /* libPods-KingfisherTests-tvOS.a */; };
 		D10EC2361C3D632300A4211C /* Kingfisher.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B2944481C3D01B20088C3E7 /* Kingfisher.framework */; };
 		D114F36E215D2D0B00A01349 /* String+MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6BC215D2BB50013BA68 /* String+MD5.swift */; };
@@ -257,6 +265,8 @@
 		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>"; };
 		4BCCF3441D5B0457003387C2 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		4BD821612189FC0C0084CC21 /* SessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDelegate.swift; sourceTree = "<group>"; };
+		4BD821662189FD330084CC21 /* SessionDataTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDataTask.swift; sourceTree = "<group>"; };
 		6CD5C0134AA4B1C0892E7319 /* Pods-KingfisherTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KingfisherTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-KingfisherTests/Pods-KingfisherTests.release.xcconfig"; sourceTree = "<group>"; };
 		7204D40BEFEA059FA25864C4 /* Pods-KingfisherTests-macOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-KingfisherTests-macOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-KingfisherTests-macOS/Pods-KingfisherTests-macOS.debug.xcconfig"; sourceTree = "<group>"; };
 		CCDD057F8DA8D24EE701CF98 /* libPods-KingfisherTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-KingfisherTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -443,6 +453,8 @@
 				D12AB69D215D2BB50013BA68 /* RequestModifier.swift */,
 				D12AB69E215D2BB50013BA68 /* Resource.swift */,
 				D12AB69F215D2BB50013BA68 /* ImageDownloader.swift */,
+				4BD821612189FC0C0084CC21 /* SessionDelegate.swift */,
+				4BD821662189FD330084CC21 /* SessionDataTask.swift */,
 				4B8E2916216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift */,
 				4B8E291B216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift */,
 				4B10480C216F157000300C61 /* ImageDataProcessor.swift */,
@@ -1023,11 +1035,13 @@
 				D1BA781F2174D07800C69D7B /* CallbackQueue.swift in Sources */,
 				D1A37BD2215D2DBA009B39B7 /* Filter.swift in Sources */,
 				D1A37BD3215D2DBA009B39B7 /* Placeholder.swift in Sources */,
+				4BD821692189FD330084CC21 /* SessionDataTask.swift in Sources */,
 				D1A37BE0215D34E8009B39B7 /* ImageDrawing.swift in Sources */,
 				D1A37BD4215D2DBA009B39B7 /* GIFAnimatedImage.swift in Sources */,
 				4B46CC61217449C600D90C4A /* MemoryStorage.swift in Sources */,
 				D1A37BD5215D2DBA009B39B7 /* RequestModifier.swift in Sources */,
 				D1A37BEA215D365A009B39B7 /* ExtenionHelpers.swift in Sources */,
+				4BD821642189FC0C0084CC21 /* SessionDelegate.swift in Sources */,
 				D1A37BD6215D2DBA009B39B7 /* Resource.swift in Sources */,
 				D1A37BD7215D2DBA009B39B7 /* ImageDownloader.swift in Sources */,
 				D1A37BD8215D2DBA009B39B7 /* ImageModifier.swift in Sources */,
@@ -1114,9 +1128,11 @@
 				D12AB6FD215D2BB50013BA68 /* UIButton+Kingfisher.swift in Sources */,
 				D12AB6E9215D2BB50013BA68 /* GIFAnimatedImage.swift in Sources */,
 				D13646752165A1A100A33652 /* Result.swift in Sources */,
+				4BD821632189FC0C0084CC21 /* SessionDelegate.swift in Sources */,
 				D12AB6E1215D2BB50013BA68 /* Filter.swift in Sources */,
 				D12AB6C5215D2BB50013BA68 /* Resource.swift in Sources */,
 				D1A37BDF215D34E8009B39B7 /* ImageDrawing.swift in Sources */,
+				4BD821682189FD330084CC21 /* SessionDataTask.swift in Sources */,
 				D12AB709215D2BB50013BA68 /* KingfisherError.swift in Sources */,
 				D1A37BEE215D375F009B39B7 /* Deprecated.swift in Sources */,
 				D12AB725215D2BB50013BA68 /* Box.swift in Sources */,
@@ -1135,8 +1151,10 @@
 				D12AB71B215D2BB50013BA68 /* CacheSerializer.swift in Sources */,
 				4B46CC6C21744AC500D90C4A /* DiskStorage.swift in Sources */,
 				D12AB6DB215D2BB50013BA68 /* ImageTransition.swift in Sources */,
+				4BD821652189FC0C0084CC21 /* SessionDelegate.swift in Sources */,
 				D1BA78202174D07800C69D7B /* CallbackQueue.swift in Sources */,
 				4B8E291A216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift in Sources */,
+				4BD8216A2189FD330084CC21 /* SessionDataTask.swift in Sources */,
 				D12AB6DF215D2BB50013BA68 /* ImageProcessor.swift in Sources */,
 				D12AB6D7215D2BB50013BA68 /* Image.swift in Sources */,
 				D12AB72B215D2BB50013BA68 /* String+MD5.swift in Sources */,
@@ -1201,9 +1219,11 @@
 				D12AB6FC215D2BB50013BA68 /* UIButton+Kingfisher.swift in Sources */,
 				D12AB6E8215D2BB50013BA68 /* GIFAnimatedImage.swift in Sources */,
 				D13646742165A1A100A33652 /* Result.swift in Sources */,
+				4BD821622189FC0C0084CC21 /* SessionDelegate.swift in Sources */,
 				D12AB6E0215D2BB50013BA68 /* Filter.swift in Sources */,
 				D12AB6C4215D2BB50013BA68 /* Resource.swift in Sources */,
 				D1A37BDE215D34E8009B39B7 /* ImageDrawing.swift in Sources */,
+				4BD821672189FD330084CC21 /* SessionDataTask.swift in Sources */,
 				D12AB708215D2BB50013BA68 /* KingfisherError.swift in Sources */,
 				D1A37BED215D375F009B39B7 /* Deprecated.swift in Sources */,
 				D12AB724215D2BB50013BA68 /* Box.swift in Sources */,

+ 9 - 4
Sources/Networking/ImageDataProcessor.swift

@@ -30,17 +30,24 @@ class ImageDataProcessor {
     let data: Data
     let callbacks: [SessionDataTask.TaskCallback]
 
+    // Note: We have an optimization choice there, to reduce queue dispatch by checking callback
+    // queue settings in each option...
     let onImageProcessed = Delegate<(Result<Image>, SessionDataTask.TaskCallback), Void>()
 
-    init(data: Data, callbacks: [SessionDataTask.TaskCallback]) {
+    private let processQueue: DispatchQueue
+
+    init(name: String, data: Data, callbacks: [SessionDataTask.TaskCallback]) {
+        self.processQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process.\(name)")
         self.data = data
         self.callbacks = callbacks
     }
 
     func process() {
+        processQueue.async(execute: doProcess)
+    }
 
+    private func doProcess() {
         var processedImages = [String: Image]()
-
         for callback in callbacks {
             let processor = callback.options.processor
             var image = processedImages[processor.identifier]
@@ -62,7 +69,5 @@ class ImageDataProcessor {
                 onImageProcessed.call((.failure(error), callback))
             }
         }
-
-
     }
 }

+ 95 - 318
Sources/Networking/ImageDownloader.swift

@@ -30,29 +30,53 @@ import AppKit
 import UIKit
 #endif
 
+/// Represents a success result of an image downloading progess.
 public struct ImageDownloadResult {
+
+    /// The downloaded image.
     public let image: Image
+
+    /// Original URL of the image request.
     public let url: URL
+
+    /// The raw data received from downloader.
     public let originalData: Data
 }
 
+/// Represents a task of an image downloading process.
 public struct DownloadTask {
+
+    // Multiple `DownloadTask`s could refer to a same `sessionTask`. This is an optimization in Kingfisher to
+    // prevent multiple downloading task for the same URL resource at the same time.
     let sessionTask: SessionDataTask
+
+    // Callbacks for this `DownloadTask` needs to be identified with the `CancelToken`.
     let cancelToken: SessionDataTask.CancelToken
 
+    /// Cancel this task if it is running. It will do nothing if this task is not running.
+    ///
+    /// - Note:
+    /// In Kingfisher, there is an optimization to prevent starting another download task if the target URL is being
+    /// downloading. However, even when internally no new session task created, a `DownloadTask` will be still created
+    /// and returned when you call related methods, but it will share the session downloading task with a previous task.
+    /// In this case, if multiple `DownloadTask`s share a single session download task, cancelling a `DownloadTask`
+    /// does not affect other `DownloadTask`s.
+    ///
+    /// If you need to cancel all `DownloadTask`s of a url, use `ImageDownloader.cancel(url:)`. If you need to cancel
+    /// all downloading tasks of an `ImageDownloader`, use `ImageDownloader.cancelAll()`.
     public func cancel() {
         sessionTask.cancel(token: cancelToken)
     }
 }
 
-/// `ImageDownloader` represents a downloading manager for requesting the image with a URL from server.
+/// Represents a downloading manager for requesting the image with a URL from server.
 open class ImageDownloader {
 
     /// The default downloader.
     public static let `default` = ImageDownloader(name: "default")
 
     // MARK: - Public property
-    /// The duration before the download is timeout. Default is 15 seconds.
+    /// The duration before the downloading is timeout. Default is 15 seconds.
     open var downloadTimeout: TimeInterval = 15.0
     
     /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this
@@ -85,7 +109,7 @@ open class ImageDownloader {
     /// Downloader will forward the received authentication challenge for the downloading session to this responder.
     open weak var authenticationChallengeResponder: AuthenticationChallengeResponsable?
 
-    let processQueue: DispatchQueue
+    private let name: String
     private let sessionHandler: SessionDelegate
     private var session: URLSession
 
@@ -97,8 +121,8 @@ open class ImageDownloader {
             fatalError("[Kingfisher] You should specify a name for the downloader. "
                 + "A downloader with empty name is not permitted.")
         }
-        
-        processQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process.\(name)")
+
+        self.name = name
         sessionHandler = SessionDelegate()
         session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: nil)
         authenticationChallengeResponder = self
@@ -134,58 +158,82 @@ open class ImageDownloader {
         }
     }
 
-    /// Download an image with a URL and option.
+    /// Downloads an image with a URL and option.
     ///
     /// - Parameters:
     ///   - url: Target URL.
     ///   - options: The options could control download behavior. See `KingfisherOptionsInfo`.
-    ///   - progressBlock: Called when the download progress updated.
-    ///   - completionHandler: Called when the download progress finishes.
-    /// - Returns: A downloading task. You could call `cancel` on it to stop the downloading process.
+    ///   - progressBlock: Called when the download progress updated. This block will be always be called in main queue.
+    ///   - completionHandler: Called when the download progress finishes. This block will be called in the queue
+    ///                        defined in `.callbackQueue` in `options` parameter.
+    /// - Returns: A downloading task. You could call `cancel` on it to stop the download task.
     @discardableResult
     open func downloadImage(with url: URL,
                             options: KingfisherOptionsInfo? = nil,
                             progressBlock: DownloadProgressBlock? = nil,
                             completionHandler: ((Result<ImageDownloadResult>) -> Void)? = nil) -> DownloadTask?
     {
+        // Creates default request.
         var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout)
         request.httpShouldUsePipelining = requestsUsePipelining
 
         let options = options ?? .empty
 
+        // Modifies request before sending.
         guard let r = options.modifier.modified(for: request) else {
-            completionHandler?(.failure(KingfisherError.requestError(reason: .emptyRequest)))
+            options.callbackQueue.execute {
+                completionHandler?(.failure(KingfisherError.requestError(reason: .emptyRequest)))
+            }
             return nil
         }
         request = r
         
         // There is a possibility that request modifier changed the url to `nil` or empty.
+        // In this case, throw an error.
         guard let url = request.url, !url.absoluteString.isEmpty else {
-            completionHandler?(.failure(KingfisherError.requestError(reason: .invalidURL(request: request))))
+            options.callbackQueue.execute {
+                completionHandler?(.failure(KingfisherError.requestError(reason: .invalidURL(request: request))))
+            }
             return nil
         }
 
-        let onProgress = Delegate<(Int64, Int64), Void>()
-        onProgress.delegate(on: self) { (_, progress) in
-            let (downloaded, total) = progress
-            progressBlock?(downloaded, total)
+        // Wraps `progressBlock` and `completionHandler` to `onProgress` and `onCompleted` respectively.
+        let onProgress = progressBlock.map {
+            block -> Delegate<(Int64, Int64), Void> in
+            let delegate = Delegate<(Int64, Int64), Void>()
+            delegate.delegate(on: self) { (_, progress) in
+                let (downloaded, total) = progress
+                block(downloaded, total)
+            }
+            return delegate
         }
 
-        let onCompleted = Delegate<Result<ImageDownloadResult>, Void>()
-        onCompleted.delegate(on: self) { (_, result) in
-            completionHandler?(result)
+        let onCompleted = completionHandler.map {
+            block -> Delegate<Result<ImageDownloadResult>, Void> in
+            let delegate =  Delegate<Result<ImageDownloadResult>, Void>()
+            delegate.delegate(on: self) { (_, result) in
+                block(result)
+            }
+            return delegate
         }
 
+        // SessionDataTask.TaskCallback is a wrapper for `onProgress`, `onCompleted` and `options` (for processor info)
         let callback = SessionDataTask.TaskCallback(
             onProgress: onProgress, onCompleted: onCompleted, options: options)
 
-        let downloadTask = sessionHandler.add(
-            request, in: session,
-            priority: options.downloadPriority,
-            callback: callback)
-        let task = downloadTask.sessionTask
-        task.onTaskDone.delegate(on: self) { (self, done) in
+        // Ready to start download. Add it to session task manager (`sessionHandler`)
+        let dataTask = session.dataTask(with: request)
+        dataTask.priority = options.downloadPriority
+
+        let downloadTask = sessionHandler.add(dataTask, url: url, callback: callback)
+
+        let sessionTask = downloadTask.sessionTask
+        sessionTask.onTaskDone.delegate(on: self) { (self, done) in
+            // Underlying downloading finishes.
+            // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback]
             let (result, callbacks) = done
+
+            // Before processing the downloaded data.
             self.delegate?.imageDownloader(
                 self,
                 didFinishDownloadingImageForURL: url,
@@ -193,10 +241,13 @@ open class ImageDownloader {
                 error: result.error)
 
             switch result {
+            // Download finished. Now process the data to an image.
             case .success(let (data, response)):
-                let prosessor = ImageDataProcessor(data: data, callbacks: callbacks)
+                let prosessor = ImageDataProcessor(name: self.name, data: data, callbacks: callbacks)
                 prosessor.onImageProcessed.delegate(on: self) { (self, result) in
-
+                    // `onImageProcessed` will be called for `callbacks.count` times, with each
+                    // `SessionDataTask.TaskCallback` as the input parameter.
+                    // result: Result<Image>, callback: SessionDataTask.TaskCallback
                     let (result, callback) = result
 
                     if let image = result.value {
@@ -207,7 +258,7 @@ open class ImageDownloader {
                     let queue = callback.options.callbackQueue
                     queue.execute { callback.onCompleted?.call(imageResult) }
                 }
-                self.processQueue.async { prosessor.process() }
+                prosessor.process()
 
             case .failure(let error):
                 callbacks.forEach { callback in
@@ -217,9 +268,10 @@ open class ImageDownloader {
             }
         }
 
-        if !task.started {
+        // Start the session task if not started yet.
+        if !sessionTask.started {
             delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
-            task.resume()
+            sessionTask.resume()
         }
         return downloadTask
     }
@@ -228,302 +280,27 @@ open class ImageDownloader {
 // MARK: - Download method
 extension ImageDownloader {
 
-    /// Cancel all downloading tasks. It will trigger the completion handlers for all not-yet-finished
-    /// downloading tasks with an NSURLErrorCancelled error.
+    /// Cancel all downloading tasks for this `ImageDownloader`. It will trigger the completion handlers
+    /// for all not-yet-finished downloading tasks.
     ///
-    /// If you need to only cancel a certain task, call `cancel()` on the `RetrieveImageDownloadTask`
-    /// returned by the downloading methods.
+    /// If you need to only cancel a certain task, call `cancel()` on the `DownloadTask`
+    /// returned by the downloading methods. If you need to cancel all `DownloadTask`s of a certain url,
+    /// use `ImageDownloader.cancel(url:)`.
     public func cancelAll() {
         sessionHandler.cancelAll()
     }
+
+    /// Cancel all downloading tasks for a given URL. It will trigger the completion handlers for
+    /// all not-yet-finished downloading tasks for the URL.
+    ///
+    /// - Parameter url: The URL which you want to cancel downloading.
+    public func cancel(url: URL) {
+        sessionHandler.cancel(url: url)
+    }
 }
 
+// Use the default implementation from extension of `AuthenticationChallengeResponsable`.
 extension ImageDownloader: AuthenticationChallengeResponsable {}
 
 // Placeholder. For retrieving extension methods of ImageDownloaderDelegate
 extension ImageDownloader: ImageDownloaderDelegate {}
-
-class SessionDelegate: NSObject {
-
-    private var tasks: [URL: SessionDataTask] = [:]
-    private let lock = NSLock()
-
-    let onValidStatusCode = Delegate<Int, Bool>()
-    let onDownloadingFinished = Delegate<(URL, Result<URLResponse>), Void>()
-    let onDidDownloadData = Delegate<SessionDataTask, Data?>()
-
-    let onReceiveSessionChallenge = Delegate<(
-            URLSession,
-            URLAuthenticationChallenge,
-            (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
-        ),
-        Void>()
-
-    let onReceiveSessionTaskChallenge = Delegate<(
-            URLSession,
-            URLSessionTask,
-            URLAuthenticationChallenge,
-            (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
-        ),
-        Void>()
-
-    func add(
-        _ requst: URLRequest,
-        in session: URLSession,
-        priority: Float,
-        callback: SessionDataTask.TaskCallback) -> DownloadTask
-    {
-
-        lock.lock()
-        defer { lock.unlock() }
-
-        let url = requst.url!
-        if let task = tasks[url] {
-            let token = task.addCallback(callback)
-            return DownloadTask(sessionTask: task, cancelToken: token)
-        } else {
-            let task = SessionDataTask(session: session, request: requst, priority: priority)
-            task.onTaskCancelled.delegate(on: self) { [unowned task] (self, value) in
-                let (token, callback) = value
-                let error = KingfisherError.requestError(reason: .taskCancelled(task: task, token: token))
-                task.onTaskDone.call((.failure(error), [callback]))
-                if !task.containsCallbacks {
-                    self.tasks[url] = nil
-                }
-            }
-            let token = task.addCallback(callback)
-            tasks[url] = task
-            return DownloadTask(sessionTask: task, cancelToken: token)
-        }
-    }
-    
-    func remove(_ task: URLSessionTask) {
-        guard let url = task.originalRequest?.url else {
-            return
-        }
-        lock.lock()
-        defer { lock.unlock() }
-        tasks[url] = nil
-    }
-    
-    func task(for task: URLSessionTask) -> SessionDataTask? {
-        guard let url = task.originalRequest?.url else {
-            return nil
-        }
-        guard let sessionTask = tasks[url] else {
-            return nil
-        }
-        guard sessionTask.task.taskIdentifier == task.taskIdentifier else {
-            return nil
-        }
-        return sessionTask
-    }
-
-    func cancelAll() {
-        lock.lock()
-        defer { lock.unlock() }
-        for task in tasks.values {
-            task.forceCancel()
-        }
-    }
-}
-
-extension SessionDelegate: URLSessionDataDelegate {
-
-    func urlSession(
-        _ session: URLSession,
-        dataTask: URLSessionDataTask,
-        didReceive response: URLResponse,
-        completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
-    {
-        lock.lock()
-        defer { lock.unlock() }
-
-        guard let httpResponse = response as? HTTPURLResponse else {
-            let error = KingfisherError.responseError(reason: .invalidURLResponse(response: response))
-            onCompleted(task: dataTask, result: .failure(error))
-            completionHandler(.cancel)
-            return
-        }
-
-        let httpStatusCode = httpResponse.statusCode
-        guard onValidStatusCode.call(httpStatusCode) == true else {
-            let error = KingfisherError.responseError(reason: .invalidHTTPStatusCode(response: httpResponse))
-            onCompleted(task: dataTask, result: .failure(error))
-            completionHandler(.cancel)
-            return
-        }
-        completionHandler(.allow)
-    }
-
-    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
-        lock.lock()
-        defer { lock.unlock() }
-
-        guard let task = self.task(for: dataTask) else {
-            return
-        }
-        task.didReceiveData(data)
-
-        if let expectedContentLength = dataTask.response?.expectedContentLength, expectedContentLength != -1 {
-            DispatchQueue.main.async {
-                task.callbacks.forEach { callback in
-                    callback.onProgress?.call((Int64(task.mutableData.count), expectedContentLength))
-                }
-            }
-        }
-    }
-
-    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
-        lock.lock()
-        defer { lock.unlock() }
-
-        guard let sessionTask = self.task(for: task) else {
-            return
-        }
-
-        if let url = task.originalRequest?.url {
-            let result: Result<(URLResponse)>
-            if let error = error {
-                result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error)))
-            } else if let response = task.response {
-                result = .success(response)
-            } else {
-                result = .failure(KingfisherError.responseError(reason: .noURLResponse))
-            }
-            onDownloadingFinished.call((url, result))
-        }
-
-        let result: Result<(Data, URLResponse?)>
-        if let error = error {
-            result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error)))
-        } else {
-            if let data = onDidDownloadData.call(sessionTask), let finalData = data {
-                result = .success((finalData, task.response))
-            } else {
-                result = .failure(KingfisherError.responseError(reason: .dataModifyingFailed(task: sessionTask)))
-            }
-        }
-        onCompleted(task: task, result: result)
-    }
-
-    func urlSession(
-        _ session: URLSession,
-        didReceive challenge: URLAuthenticationChallenge,
-        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
-    {
-        onReceiveSessionChallenge.call((session, challenge, completionHandler))
-    }
-
-    func urlSession(
-        _ session: URLSession,
-        task: URLSessionTask,
-        didReceive challenge: URLAuthenticationChallenge,
-        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
-    {
-        onReceiveSessionTaskChallenge.call((session, task, challenge, completionHandler))
-    }
-
-    private func onCompleted(task: URLSessionTask, result: Result<(Data, URLResponse?)>) {
-        guard let sessionTask = self.task(for: task) else {
-            return
-        }
-        onCompleted(sessionTask: sessionTask, result: result)
-    }
-
-    private func onCompleted(sessionTask: SessionDataTask, result: Result<(Data, URLResponse?)>) {
-        guard let url = sessionTask.task.originalRequest?.url else {
-            return
-        }
-        tasks[url] = nil
-        sessionTask.onTaskDone.call((result, Array(sessionTask.callbacks)))
-    }
-}
-
-public class SessionDataTask {
-
-    public typealias CancelToken = Int
-
-    struct TaskCallback {
-        let onProgress: Delegate<(Int64, Int64), Void>?
-        let onCompleted: Delegate<Result<ImageDownloadResult>, Void>?
-        let options: KingfisherOptionsInfo
-    }
-    
-    public private(set) var mutableData: Data
-    public let task: URLSessionDataTask
-    
-    private var callbacksStore = [CancelToken: TaskCallback]()
-
-    var callbacks: Dictionary<SessionDataTask.CancelToken, SessionDataTask.TaskCallback>.Values {
-        return callbacksStore.values
-    }
-
-    var currentToken = 0
-
-    private let lock = NSLock()
-
-    let onTaskDone = Delegate<(Result<(Data, URLResponse?)>, [TaskCallback]), Void>()
-    let onTaskCancelled = Delegate<(CancelToken, TaskCallback), Void>()
-
-    var started = false
-    var containsCallbacks: Bool {
-        // We should be able to use `task.state != .running` to check it.
-        // However, in some rare cases, cancelling the task does not change
-        // task state to `.cancelling`, but still in `.running`. So we need
-        // to check callbacks count to for sure that it is safe to remove the
-        // task in delegate.
-        return !callbacks.isEmpty
-    }
-    
-    init(session: URLSession, request: URLRequest, priority: Float) {
-        task = session.dataTask(with: request)
-        task.priority = priority
-        mutableData = Data()
-    }
-
-    func addCallback(_ callback: TaskCallback) -> CancelToken {
-        lock.lock()
-        defer { lock.unlock() }
-        callbacksStore[currentToken] = callback
-        defer { currentToken += 1 }
-        return currentToken
-    }
-
-    func removeCallback(_ token: CancelToken) -> TaskCallback? {
-        lock.lock()
-        defer { lock.unlock() }
-        if let callback = callbacksStore[token] {
-            callbacksStore[token] = nil
-            return callback
-        }
-        return nil
-    }
-    
-    func resume() {
-        started = true
-        task.resume()
-    }
-
-    func cancel(token: CancelToken) {
-        let result = removeCallback(token)
-        if let callback = result {
-
-            if callbacksStore.count == 0 {
-                task.cancel()
-            }
-
-            onTaskCancelled.call((token, callback))
-        }
-    }
-
-    func forceCancel() {
-        for token in callbacksStore.keys {
-            cancel(token: token)
-        }
-    }
-    
-    func didReceiveData(_ data: Data) {
-        mutableData.append(data)
-    }
-}

+ 114 - 0
Sources/Networking/SessionDataTask.swift

@@ -0,0 +1,114 @@
+//
+//  SessionDataTask.swift
+//  Kingfisher
+//
+//  Created by Wei Wang on 2018/11/1.
+//
+//  Copyright (c) 2018年 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 Foundation
+
+public class SessionDataTask {
+
+    public typealias CancelToken = Int
+
+    struct TaskCallback {
+        let onProgress: Delegate<(Int64, Int64), Void>?
+        let onCompleted: Delegate<Result<ImageDownloadResult>, Void>?
+        let options: KingfisherOptionsInfo
+    }
+
+    public private(set) var mutableData: Data
+    public let task: URLSessionDataTask
+
+    private var callbacksStore = [CancelToken: TaskCallback]()
+
+    var callbacks: Dictionary<SessionDataTask.CancelToken, SessionDataTask.TaskCallback>.Values {
+        return callbacksStore.values
+    }
+
+    var currentToken = 0
+
+    private let lock = NSLock()
+
+    let onTaskDone = Delegate<(Result<(Data, URLResponse?)>, [TaskCallback]), Void>()
+    let onTaskCancelled = Delegate<(CancelToken, TaskCallback), Void>()
+
+    var started = false
+    var containsCallbacks: Bool {
+        // We should be able to use `task.state != .running` to check it.
+        // However, in some rare cases, cancelling the task does not change
+        // task state to `.cancelling`, but still in `.running`. So we need
+        // to check callbacks count to for sure that it is safe to remove the
+        // task in delegate.
+        return !callbacks.isEmpty
+    }
+
+    init(task: URLSessionDataTask) {
+        self.task = task
+        mutableData = Data()
+    }
+
+    func addCallback(_ callback: TaskCallback) -> CancelToken {
+        lock.lock()
+        defer { lock.unlock() }
+        callbacksStore[currentToken] = callback
+        defer { currentToken += 1 }
+        return currentToken
+    }
+
+    func removeCallback(_ token: CancelToken) -> TaskCallback? {
+        lock.lock()
+        defer { lock.unlock() }
+        if let callback = callbacksStore[token] {
+            callbacksStore[token] = nil
+            return callback
+        }
+        return nil
+    }
+
+    func resume() {
+        started = true
+        task.resume()
+    }
+
+    func cancel(token: CancelToken) {
+        let result = removeCallback(token)
+        if let callback = result {
+
+            if callbacksStore.count == 0 {
+                task.cancel()
+            }
+
+            onTaskCancelled.call((token, callback))
+        }
+    }
+
+    func forceCancel() {
+        for token in callbacksStore.keys {
+            cancel(token: token)
+        }
+    }
+
+    func didReceiveData(_ data: Data) {
+        mutableData.append(data)
+    }
+}

+ 229 - 0
Sources/Networking/SessionDelegate.swift

@@ -0,0 +1,229 @@
+//
+//  SessionDelegate.swift
+//  Kingfisher
+//
+//  Created by Wei Wang on 2018/11/1.
+//
+//  Copyright (c) 2018年 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 Foundation
+
+class SessionDelegate: NSObject {
+
+    typealias SessionChallengeFunc = (
+        URLSession,
+        URLAuthenticationChallenge,
+        (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
+    )
+
+    typealias SessionTaskChallengeFunc = (
+        URLSession,
+        URLSessionTask,
+        URLAuthenticationChallenge,
+        (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
+    )
+
+    private var tasks: [URL: SessionDataTask] = [:]
+    private let lock = NSLock()
+
+    let onValidStatusCode = Delegate<Int, Bool>()
+    let onDownloadingFinished = Delegate<(URL, Result<URLResponse>), Void>()
+    let onDidDownloadData = Delegate<SessionDataTask, Data?>()
+
+    let onReceiveSessionChallenge = Delegate<SessionChallengeFunc, Void>()
+    let onReceiveSessionTaskChallenge = Delegate<SessionTaskChallengeFunc, Void>()
+
+    func add(
+        _ dataTask: URLSessionDataTask,
+        url: URL,
+        callback: SessionDataTask.TaskCallback) -> DownloadTask
+    {
+        lock.lock()
+        defer { lock.unlock() }
+
+        if let task = tasks[url] {
+            let token = task.addCallback(callback)
+            return DownloadTask(sessionTask: task, cancelToken: token)
+        } else {
+            let task = SessionDataTask(task: dataTask)
+            task.onTaskCancelled.delegate(on: self) { [unowned task] (self, value) in
+                let (token, callback) = value
+                let error = KingfisherError.requestError(reason: .taskCancelled(task: task, token: token))
+                task.onTaskDone.call((.failure(error), [callback]))
+                if !task.containsCallbacks {
+                    self.tasks[url] = nil
+                }
+            }
+            let token = task.addCallback(callback)
+            tasks[url] = task
+            return DownloadTask(sessionTask: task, cancelToken: token)
+        }
+    }
+
+    func remove(_ task: URLSessionTask) {
+        guard let url = task.originalRequest?.url else {
+            return
+        }
+        lock.lock()
+        defer { lock.unlock() }
+        tasks[url] = nil
+    }
+
+    func task(for task: URLSessionTask) -> SessionDataTask? {
+        guard let url = task.originalRequest?.url else {
+            return nil
+        }
+        guard let sessionTask = tasks[url] else {
+            return nil
+        }
+        guard sessionTask.task.taskIdentifier == task.taskIdentifier else {
+            return nil
+        }
+        return sessionTask
+    }
+
+    func cancelAll() {
+        lock.lock()
+        defer { lock.unlock() }
+        for task in tasks.values {
+            task.forceCancel()
+        }
+    }
+
+    func cancel(url: URL) {
+        lock.lock()
+        defer { lock.unlock() }
+        let task = tasks[url]
+        task?.forceCancel()
+    }
+}
+
+extension SessionDelegate: URLSessionDataDelegate {
+
+    func urlSession(
+        _ session: URLSession,
+        dataTask: URLSessionDataTask,
+        didReceive response: URLResponse,
+        completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
+    {
+        lock.lock()
+        defer { lock.unlock() }
+
+        guard let httpResponse = response as? HTTPURLResponse else {
+            let error = KingfisherError.responseError(reason: .invalidURLResponse(response: response))
+            onCompleted(task: dataTask, result: .failure(error))
+            completionHandler(.cancel)
+            return
+        }
+
+        let httpStatusCode = httpResponse.statusCode
+        guard onValidStatusCode.call(httpStatusCode) == true else {
+            let error = KingfisherError.responseError(reason: .invalidHTTPStatusCode(response: httpResponse))
+            onCompleted(task: dataTask, result: .failure(error))
+            completionHandler(.cancel)
+            return
+        }
+        completionHandler(.allow)
+    }
+
+    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
+        lock.lock()
+        defer { lock.unlock() }
+
+        guard let task = self.task(for: dataTask) else {
+            return
+        }
+        task.didReceiveData(data)
+
+        if let expectedContentLength = dataTask.response?.expectedContentLength, expectedContentLength != -1 {
+            DispatchQueue.main.async {
+                task.callbacks.forEach { callback in
+                    callback.onProgress?.call((Int64(task.mutableData.count), expectedContentLength))
+                }
+            }
+        }
+    }
+
+    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
+        lock.lock()
+        defer { lock.unlock() }
+
+        guard let sessionTask = self.task(for: task) else {
+            return
+        }
+
+        if let url = task.originalRequest?.url {
+            let result: Result<(URLResponse)>
+            if let error = error {
+                result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error)))
+            } else if let response = task.response {
+                result = .success(response)
+            } else {
+                result = .failure(KingfisherError.responseError(reason: .noURLResponse))
+            }
+            onDownloadingFinished.call((url, result))
+        }
+
+        let result: Result<(Data, URLResponse?)>
+        if let error = error {
+            result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error)))
+        } else {
+            if let data = onDidDownloadData.call(sessionTask), let finalData = data {
+                result = .success((finalData, task.response))
+            } else {
+                result = .failure(KingfisherError.responseError(reason: .dataModifyingFailed(task: sessionTask)))
+            }
+        }
+        onCompleted(task: task, result: result)
+    }
+
+    func urlSession(
+        _ session: URLSession,
+        didReceive challenge: URLAuthenticationChallenge,
+        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
+    {
+        onReceiveSessionChallenge.call((session, challenge, completionHandler))
+    }
+
+    func urlSession(
+        _ session: URLSession,
+        task: URLSessionTask,
+        didReceive challenge: URLAuthenticationChallenge,
+        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
+    {
+        onReceiveSessionTaskChallenge.call((session, task, challenge, completionHandler))
+    }
+
+    private func onCompleted(task: URLSessionTask, result: Result<(Data, URLResponse?)>) {
+        guard let sessionTask = self.task(for: task) else {
+            return
+        }
+        onCompleted(sessionTask: sessionTask, result: result)
+    }
+
+    private func onCompleted(sessionTask: SessionDataTask, result: Result<(Data, URLResponse?)>) {
+        guard let url = sessionTask.task.originalRequest?.url else {
+            return
+        }
+        tasks[url] = nil
+        sessionTask.onTaskDone.call((result, Array(sessionTask.callbacks)))
+    }
+}