Skip to content

Commit 184b827

Browse files
authored
Merge pull request #234 from taish/new-feature-upload-progress
Add a way to get progress of uploading.
2 parents 5f67418 + 8748820 commit 184b827

File tree

7 files changed

+102
-36
lines changed

7 files changed

+102
-36
lines changed

Sources/APIKit/Session.swift

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ open class Session {
3737
/// - parameter handler: The closure that receives result of the request.
3838
/// - returns: The new session task.
3939
@discardableResult
40-
open class func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
41-
return shared.send(request, callbackQueue: callbackQueue, handler: handler)
40+
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? {
41+
return shared.send(request, callbackQueue: callbackQueue, progressHandler: progressHandler, completionHandler: completionHandler)
4242
}
4343

4444
/// Calls `cancelRequests(with:passingTest:)` of `sharedSession`.
@@ -55,41 +55,46 @@ open class Session {
5555
/// - parameter handler: The closure that receives result of the request.
5656
/// - returns: The new session task.
5757
@discardableResult
58-
open func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil, handler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
58+
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? {
5959
let callbackQueue = callbackQueue ?? self.callbackQueue
6060

6161
let urlRequest: URLRequest
6262
do {
6363
urlRequest = try request.buildURLRequest()
6464
} catch {
6565
callbackQueue.execute {
66-
handler(.failure(.requestError(error)))
66+
completionHandler(.failure(.requestError(error)))
6767
}
6868
return nil
6969
}
7070

71-
let task = adapter.createTask(with: urlRequest) { data, urlResponse, error in
72-
let result: Result<Request.Response, SessionTaskError>
73-
74-
switch (data, urlResponse, error) {
75-
case (_, _, let error?):
76-
result = .failure(.connectionError(error))
71+
let task = adapter.createTask(with: urlRequest,
72+
progressHandler: { bytesSent, totalBytesSent, totalBytesExpectedToSend in
73+
progressHandler(bytesSent, totalBytesSent, totalBytesExpectedToSend)
74+
},
75+
completionHandler: { data, urlResponse, error in
76+
let result: Result<Request.Response, SessionTaskError>
77+
78+
switch (data, urlResponse, error) {
79+
case (_, _, let error?):
80+
result = .failure(.connectionError(error))
81+
82+
case (let data?, let urlResponse as HTTPURLResponse, _):
83+
do {
84+
result = .success(try request.parse(data: data as Data, urlResponse: urlResponse))
85+
} catch {
86+
result = .failure(.responseError(error))
87+
}
7788

78-
case (let data?, let urlResponse as HTTPURLResponse, _):
79-
do {
80-
result = .success(try request.parse(data: data as Data, urlResponse: urlResponse))
81-
} catch {
82-
result = .failure(.responseError(error))
89+
default:
90+
result = .failure(.responseError(ResponseError.nonHTTPURLResponse(urlResponse)))
8391
}
8492

85-
default:
86-
result = .failure(.responseError(ResponseError.nonHTTPURLResponse(urlResponse)))
87-
}
88-
89-
callbackQueue.execute {
90-
handler(result)
93+
callbackQueue.execute {
94+
completionHandler(result)
95+
}
9196
}
92-
}
97+
)
9398

9499
setRequest(request, forTask: task)
95100
task.resume()

Sources/APIKit/SessionAdapter/SessionAdapter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public protocol SessionTask: class {
1111
/// with `Session`.
1212
public protocol SessionAdapter {
1313
/// Returns instance that conforms to `SessionTask`. `handler` must be called after success or failure.
14-
func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask
14+
func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask
1515

1616
/// Collects tasks from backend networking stack. `handler` must be called after collecting.
1717
func getTasks(with handler: @escaping ([SessionTask]) -> Void)

Sources/APIKit/SessionAdapter/URLSessionAdapter.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ extension URLSessionTask: SessionTask {
66

77
private var dataTaskResponseBufferKey = 0
88
private var taskAssociatedObjectCompletionHandlerKey = 0
9+
private var taskAssociatedObjectProgressHandlerKey = 0
910

1011
/// `URLSessionAdapter` connects `URLSession` with `Session`.
1112
///
@@ -25,11 +26,12 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS
2526
}
2627

2728
/// Creates `URLSessionDataTask` instance using `dataTaskWithRequest(_:completionHandler:)`.
28-
open func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask {
29+
open func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask {
2930
let task = urlSession.dataTask(with: URLRequest)
3031

3132
setBuffer(NSMutableData(), forTask: task)
32-
setHandler(handler, forTask: task)
33+
setHandler(completionHandler, forTask: task)
34+
setProgressHandler(progressHandler, forTask: task)
3335

3436
return task
3537
}
@@ -61,6 +63,13 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS
6163
return objc_getAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey) as? (Data?, URLResponse?, Error?) -> Void
6264
}
6365

66+
private func setProgressHandler(_ progressHandler: @escaping (Int64, Int64, Int64) -> Void, forTask task: URLSessionTask) {
67+
objc_setAssociatedObject(task, &taskAssociatedObjectProgressHandlerKey, progressHandler as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
68+
}
69+
70+
private func progressHandler(for task: URLSessionTask) -> ((Int64, Int64, Int64) -> Void)? {
71+
return objc_getAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey) as? (Int64, Int64, Int64) -> Void
72+
}
6473
// MARK: URLSessionTaskDelegate
6574
open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
6675
handler(for: task)?(buffer(for: task) as Data?, task.response, error)
@@ -70,4 +79,9 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS
7079
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
7180
buffer(for: dataTask)?.append(data)
7281
}
82+
83+
// MARK: URLSessionDataDelegate
84+
open func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
85+
progressHandler(for: task)?(bytesSent, totalBytesSent, totalBytesExpectedToSend)
86+
}
7387
}

Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ class URLSessionAdapterSubclassTests: XCTestCase {
1515
functionCallFlags[(#function)] = true
1616
super.urlSession(session, dataTask: dataTask, didReceive: data)
1717
}
18+
19+
override func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
20+
functionCallFlags[(#function)] = true
21+
super.urlSession(session, task: task, didSendBodyData: bytesSent, totalBytesSent: totalBytesSent, totalBytesExpectedToSend: totalBytesExpectedToSend)
22+
}
1823
}
1924

2025
var adapter: SessionAdapter!
@@ -50,4 +55,25 @@ class URLSessionAdapterSubclassTests: XCTestCase {
5055
XCTAssertEqual(adapter.functionCallFlags["urlSession(_:task:didCompleteWithError:)"], true)
5156
XCTAssertEqual(adapter.functionCallFlags["urlSession(_:dataTask:didReceive:)"], true)
5257
}
58+
59+
// Limitation: 'urlSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:' delegate method will never be called when you stub the request using subclass of URLProtocol.
60+
func testDelegateProgressMethodCall() {
61+
let expectation = self.expectation(description: "wait for response")
62+
let request = TestRequest(baseURL: "https://httpbin.org", path: "/post", method: .post)
63+
let configuration = URLSessionConfiguration.default
64+
let adapter = SessionAdapter(configuration: configuration)
65+
let session = Session(adapter: adapter)
66+
67+
session.send(request,
68+
completionHandler: { result in
69+
if case .failure = result {
70+
XCTFail()
71+
}
72+
73+
expectation.fulfill()
74+
})
75+
76+
waitForExpectations(timeout: 10.0, handler: nil)
77+
XCTAssertEqual(adapter.functionCallFlags["urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)"], true)
78+
}
5379
}

Tests/APIKitTests/SessionTests.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,23 @@ class SessionTests: XCTestCase {
223223
waitForExpectations(timeout: 1.0, handler: nil)
224224
}
225225

226+
func testProgress() {
227+
let dictionary = ["key": "value"]
228+
adapter.data = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
229+
230+
let expectation = self.expectation(description: "wait for response")
231+
let request = TestRequest(method: .post)
232+
233+
session.send(request, progressHandler: { bytesSent, totalBytesSent, totalBytesExpectedToSend in
234+
XCTAssertNotNil(bytesSent)
235+
XCTAssertNotNil(totalBytesSent)
236+
XCTAssertNotNil(totalBytesExpectedToSend)
237+
expectation.fulfill()
238+
})
239+
240+
waitForExpectations(timeout: 1.0, handler: nil)
241+
}
242+
226243
// MARK: Class methods
227244
func testSharedSession() {
228245
XCTAssert(Session.shared === Session.shared)
@@ -238,12 +255,13 @@ class SessionTests: XCTestCase {
238255
return testSesssion
239256
}
240257

241-
override func send<Request : APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue?, handler: @escaping (Result<Request.Response, SessionTaskError>) -> Void) -> SessionTask? {
258+
override func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void) -> SessionTask? {
259+
242260
functionCallFlags[(#function)] = true
243261
return super.send(request)
244262
}
245263

246-
override func cancelRequests<Request : APIKit.Request>(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool) {
264+
override func cancelRequests<Request: APIKit.Request>(with requestType: Request.Type, passingTest test: @escaping (Request) -> Bool) {
247265
functionCallFlags[(#function)] = true
248266
}
249267
}
@@ -252,7 +270,7 @@ class SessionTests: XCTestCase {
252270
SessionSubclass.send(TestRequest())
253271
SessionSubclass.cancelRequests(with: TestRequest.self)
254272

255-
XCTAssertEqual(testSession.functionCallFlags["send(_:callbackQueue:handler:)"], true)
273+
XCTAssertEqual(testSession.functionCallFlags["send(_:callbackQueue:progressHandler:completionHandler:)"], true)
256274
XCTAssertEqual(testSession.functionCallFlags["cancelRequests(with:passingTest:)"], true)
257275
}
258276
}

Tests/APIKitTests/TestComponents/TestSessionAdapter.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,18 @@ class TestSessionAdapter: SessionAdapter {
4040
func executeAllTasks() {
4141
for task in tasks {
4242
if task.cancelled {
43-
task.handler(nil, nil, Error.cancelled)
43+
task.completionHandler(nil, nil, Error.cancelled)
4444
} else {
45-
task.handler(data, urlResponse, error)
45+
task.progressHandler(1, 1, 1)
46+
task.completionHandler(data, urlResponse, error)
4647
}
4748
}
4849

4950
tasks = []
5051
}
5152

52-
func createTask(with URLRequest: URLRequest, handler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask {
53-
let task = TestSessionTask(handler: handler)
53+
func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask {
54+
let task = TestSessionTask(progressHandler: progressHandler, completionHandler: completionHandler)
5455
tasks.append(task)
5556

5657
return task

Tests/APIKitTests/TestComponents/TestSessionTask.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import Foundation
22
import APIKit
33

44
class TestSessionTask: SessionTask {
5-
6-
var handler: (Data?, URLResponse?, Error?) -> Void
5+
6+
var completionHandler: (Data?, URLResponse?, Error?) -> Void
7+
var progressHandler: (Int64, Int64, Int64) -> Void
78
var cancelled = false
89

9-
init(handler: @escaping (Data?, URLResponse?, Error?) -> Void) {
10-
self.handler = handler
10+
init(progressHandler: @escaping (Int64, Int64, Int64) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
11+
self.completionHandler = completionHandler
12+
self.progressHandler = progressHandler
1113
}
1214

1315
func resume() {

0 commit comments

Comments
 (0)