Skip to content

Add a way to get progress of uploading. #234

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 27 additions & 22 deletions Sources/APIKit/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
return shared.send(request, callbackQueue: callbackQueue, handler: handler)
open class func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Int64, Int64, Int64) -> Void = { _ in }, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
return shared.send(request, callbackQueue: callbackQueue, progressHandler: progressHandler, completionHandler: completionHandler)
}

/// Calls `cancelRequests(with:passingTest:)` of `sharedSession`.
Expand All @@ -55,41 +55,46 @@ open class Session {
/// - parameter handler: The closure that receives result of the request.
/// - returns: The new session task.
@discardableResult
open func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
open func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Int64, Int64, Int64) -> Void = { _ in }, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
let callbackQueue = callbackQueue ?? self.callbackQueue

let urlRequest: URLRequest
do {
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<Request.Response, SessionTaskError>

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<Request.Response, SessionTaskError>

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()
Expand Down
2 changes: 1 addition & 1 deletion Sources/APIKit/SessionAdapter/SessionAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 16 additions & 2 deletions Sources/APIKit/SessionAdapter/URLSessionAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ extension URLSessionTask: SessionTask {

private var dataTaskResponseBufferKey = 0
private var taskAssociatedObjectCompletionHandlerKey = 0
private var taskAssociatedObjectProgressHandlerKey = 0

/// `URLSessionAdapter` connects `URLSession` with `Session`.
///
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -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)
}
}
24 changes: 21 additions & 3 deletions Tests/APIKitTests/SessionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -238,12 +255,13 @@ class SessionTests: XCTestCase {
return testSesssion
}

override func send<Request : APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue?, handler: @escaping (Result<Request.Response, SessionTaskError>) -> Void) -> SessionTask? {
override func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void) -> SessionTask? {

functionCallFlags[(#function)] = true
return super.send(request)
}

override func cancelRequests<Request : APIKit.Request>(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool) {
override func cancelRequests<Request: APIKit.Request>(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool) {
functionCallFlags[(#function)] = true
}
}
Expand All @@ -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)
}
}
9 changes: 5 additions & 4 deletions Tests/APIKitTests/TestComponents/TestSessionAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions Tests/APIKitTests/TestComponents/TestSessionTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down