diff --git a/Sources/APIKit/Session.swift b/Sources/APIKit/Session.swift index 72a9405e..9726c7b7 100644 --- a/Sources/APIKit/Session.swift +++ b/Sources/APIKit/Session.swift @@ -37,8 +37,8 @@ open class Session { /// - parameter handler: The closure that receives result of the request. /// - returns: The new session task. @discardableResult - open class func send(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { - return shared.send(request, callbackQueue: callbackQueue, handler: handler) + open class func send(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Int64, Int64, Int64) -> Void = { _ in }, completionHandler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { + return shared.send(request, callbackQueue: callbackQueue, progressHandler: progressHandler, completionHandler: completionHandler) } /// Calls `cancelRequests(with:passingTest:)` of `sharedSession`. @@ -55,7 +55,7 @@ open class Session { /// - parameter handler: The closure that receives result of the request. /// - returns: The new session task. @discardableResult - open func send(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { + open func send(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Int64, Int64, Int64) -> Void = { _ in }, completionHandler: @escaping (Result) -> Void = { _ in }) -> SessionTask? { let callbackQueue = callbackQueue ?? self.callbackQueue let urlRequest: URLRequest @@ -63,33 +63,38 @@ open class Session { urlRequest = try request.buildURLRequest() } catch { callbackQueue.execute { - handler(.failure(.requestError(error))) + completionHandler(.failure(.requestError(error))) } return nil } - let task = adapter.createTask(with: urlRequest) { data, urlResponse, error in - let result: Result - - switch (data, urlResponse, error) { - case (_, _, let error?): - result = .failure(.connectionError(error)) + let task = adapter.createTask(with: urlRequest, + progressHandler: { bytesSent, totalBytesSent, totalBytesExpectedToSend in + progressHandler(bytesSent, totalBytesSent, totalBytesExpectedToSend) + }, + completionHandler: { data, urlResponse, error in + let result: Result + + switch (data, urlResponse, error) { + case (_, _, let error?): + result = .failure(.connectionError(error)) + + case (let data?, let urlResponse as HTTPURLResponse, _): + do { + result = .success(try request.parse(data: data as Data, urlResponse: urlResponse)) + } catch { + result = .failure(.responseError(error)) + } - case (let data?, let urlResponse as HTTPURLResponse, _): - do { - result = .success(try request.parse(data: data as Data, urlResponse: urlResponse)) - } catch { - result = .failure(.responseError(error)) + default: + result = .failure(.responseError(ResponseError.nonHTTPURLResponse(urlResponse))) } - default: - result = .failure(.responseError(ResponseError.nonHTTPURLResponse(urlResponse))) - } - - callbackQueue.execute { - handler(result) + callbackQueue.execute { + completionHandler(result) + } } - } + ) setRequest(request, forTask: task) task.resume() diff --git a/Sources/APIKit/SessionAdapter/SessionAdapter.swift b/Sources/APIKit/SessionAdapter/SessionAdapter.swift index d123dae0..3db2b583 100644 --- a/Sources/APIKit/SessionAdapter/SessionAdapter.swift +++ b/Sources/APIKit/SessionAdapter/SessionAdapter.swift @@ -11,7 +11,7 @@ public protocol SessionTask: class { /// with `Session`. public protocol SessionAdapter { /// Returns instance that conforms to `SessionTask`. `handler` must be called after success or failure. - func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask + func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask /// Collects tasks from backend networking stack. `handler` must be called after collecting. func getTasks(with handler: @escaping ([SessionTask]) -> Void) diff --git a/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift b/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift index cc96ab6b..243d9394 100644 --- a/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift +++ b/Sources/APIKit/SessionAdapter/URLSessionAdapter.swift @@ -6,6 +6,7 @@ extension URLSessionTask: SessionTask { private var dataTaskResponseBufferKey = 0 private var taskAssociatedObjectCompletionHandlerKey = 0 +private var taskAssociatedObjectProgressHandlerKey = 0 /// `URLSessionAdapter` connects `URLSession` with `Session`. /// @@ -25,11 +26,12 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS } /// Creates `URLSessionDataTask` instance using `dataTaskWithRequest(_:completionHandler:)`. - open func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask { + open func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask { let task = urlSession.dataTask(with: URLRequest) setBuffer(NSMutableData(), forTask: task) - setHandler(handler, forTask: task) + setHandler(completionHandler, forTask: task) + setProgressHandler(progressHandler, forTask: task) return task } @@ -61,6 +63,13 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS return objc_getAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey) as? (Data?, URLResponse?, Error?) -> Void } + private func setProgressHandler(_ progressHandler: @escaping (Int64, Int64, Int64) -> Void, forTask task: URLSessionTask) { + objc_setAssociatedObject(task, &taskAssociatedObjectProgressHandlerKey, progressHandler as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + + private func progressHandler(for task: URLSessionTask) -> ((Int64, Int64, Int64) -> Void)? { + return objc_getAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey) as? (Int64, Int64, Int64) -> Void + } // MARK: URLSessionTaskDelegate open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { handler(for: task)?(buffer(for: task) as Data?, task.response, error) @@ -70,4 +79,9 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { buffer(for: dataTask)?.append(data) } + + // MARK: URLSessionDataDelegate + open func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + progressHandler(for: task)?(bytesSent, totalBytesSent, totalBytesExpectedToSend) + } } diff --git a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift index 4820dde3..6e03bf67 100644 --- a/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift +++ b/Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift @@ -15,6 +15,11 @@ class URLSessionAdapterSubclassTests: XCTestCase { functionCallFlags[(#function)] = true super.urlSession(session, dataTask: dataTask, didReceive: data) } + + override func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { + functionCallFlags[(#function)] = true + super.urlSession(session, task: task, didSendBodyData: bytesSent, totalBytesSent: totalBytesSent, totalBytesExpectedToSend: totalBytesExpectedToSend) + } } var adapter: SessionAdapter! @@ -50,4 +55,25 @@ class URLSessionAdapterSubclassTests: XCTestCase { XCTAssertEqual(adapter.functionCallFlags["urlSession(_:task:didCompleteWithError:)"], true) XCTAssertEqual(adapter.functionCallFlags["urlSession(_:dataTask:didReceive:)"], true) } + + // Limitation: 'urlSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:' delegate method will never be called when you stub the request using subclass of URLProtocol. + func testDelegateProgressMethodCall() { + let expectation = self.expectation(description: "wait for response") + let request = TestRequest(baseURL: "https://httpbin.org", path: "/post", method: .post) + let configuration = URLSessionConfiguration.default + let adapter = SessionAdapter(configuration: configuration) + let session = Session(adapter: adapter) + + session.send(request, + completionHandler: { result in + if case .failure = result { + XCTFail() + } + + expectation.fulfill() + }) + + waitForExpectations(timeout: 10.0, handler: nil) + XCTAssertEqual(adapter.functionCallFlags["urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)"], true) + } } diff --git a/Tests/APIKitTests/SessionTests.swift b/Tests/APIKitTests/SessionTests.swift index 138fdf9f..66e44fce 100644 --- a/Tests/APIKitTests/SessionTests.swift +++ b/Tests/APIKitTests/SessionTests.swift @@ -223,6 +223,23 @@ class SessionTests: XCTestCase { waitForExpectations(timeout: 1.0, handler: nil) } + func testProgress() { + let dictionary = ["key": "value"] + adapter.data = try! JSONSerialization.data(withJSONObject: dictionary, options: []) + + let expectation = self.expectation(description: "wait for response") + let request = TestRequest(method: .post) + + session.send(request, progressHandler: { bytesSent, totalBytesSent, totalBytesExpectedToSend in + XCTAssertNotNil(bytesSent) + XCTAssertNotNil(totalBytesSent) + XCTAssertNotNil(totalBytesExpectedToSend) + expectation.fulfill() + }) + + waitForExpectations(timeout: 1.0, handler: nil) + } + // MARK: Class methods func testSharedSession() { XCTAssert(Session.shared === Session.shared) @@ -238,12 +255,13 @@ class SessionTests: XCTestCase { return testSesssion } - override func send(_ request: Request, callbackQueue: CallbackQueue?, handler: @escaping (Result) -> Void) -> SessionTask? { + override func send(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Result) -> Void) -> SessionTask? { + functionCallFlags[(#function)] = true return super.send(request) } - override func cancelRequests(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool) { + override func cancelRequests(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool) { functionCallFlags[(#function)] = true } } @@ -252,7 +270,7 @@ class SessionTests: XCTestCase { SessionSubclass.send(TestRequest()) SessionSubclass.cancelRequests(with: TestRequest.self) - XCTAssertEqual(testSession.functionCallFlags["send(_:callbackQueue:handler:)"], true) + XCTAssertEqual(testSession.functionCallFlags["send(_:callbackQueue:progressHandler:completionHandler:)"], true) XCTAssertEqual(testSession.functionCallFlags["cancelRequests(with:passingTest:)"], true) } } diff --git a/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift b/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift index 4813c23c..b9c3cdb1 100644 --- a/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift +++ b/Tests/APIKitTests/TestComponents/TestSessionAdapter.swift @@ -40,17 +40,18 @@ class TestSessionAdapter: SessionAdapter { func executeAllTasks() { for task in tasks { if task.cancelled { - task.handler(nil, nil, Error.cancelled) + task.completionHandler(nil, nil, Error.cancelled) } else { - task.handler(data, urlResponse, error) + task.progressHandler(1, 1, 1) + task.completionHandler(data, urlResponse, error) } } tasks = [] } - func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask { - let task = TestSessionTask(handler: handler) + func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask { + let task = TestSessionTask(progressHandler: progressHandler, completionHandler: completionHandler) tasks.append(task) return task diff --git a/Tests/APIKitTests/TestComponents/TestSessionTask.swift b/Tests/APIKitTests/TestComponents/TestSessionTask.swift index 5bf7927a..af230b48 100644 --- a/Tests/APIKitTests/TestComponents/TestSessionTask.swift +++ b/Tests/APIKitTests/TestComponents/TestSessionTask.swift @@ -2,12 +2,14 @@ import Foundation import APIKit class TestSessionTask: SessionTask { - - var handler: (Data?, URLResponse?, Error?) -> Void + + var completionHandler: (Data?, URLResponse?, Error?) -> Void + var progressHandler: (Int64, Int64, Int64) -> Void var cancelled = false - init(handler: @escaping (Data?, URLResponse?, Error?) -> Void) { - self.handler = handler + init(progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) { + self.completionHandler = completionHandler + self.progressHandler = progressHandler } func resume() {