Skip to content

Commit 0d2ac6a

Browse files
authored
Merge pull request #308 from ishkawa/feature/download-progress
Add a way to get progress of downloading.
2 parents 7a2a586 + c33ac1b commit 0d2ac6a

File tree

9 files changed

+91
-38
lines changed

9 files changed

+91
-38
lines changed

Demo.playground/Contents.swift

+6-3
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,16 @@ struct GetRateLimitRequest: GitHubRequest {
6060
//: Step 4: Send request
6161
let request = GetRateLimitRequest()
6262

63-
Session.send(request) { result in
63+
Session.send(request, uploadProgressHandler: { progress in
64+
print("upload progress: \(progress.fractionCompleted)")
65+
}, downloadProgressHandler: { progress in
66+
print("download progress: \(progress.fractionCompleted) %")
67+
}, completionHandler: { result in
6468
switch result {
6569
case .success(let rateLimit):
6670
print("count: \(rateLimit.count)")
6771
print("reset: \(rateLimit.resetDate)")
68-
6972
case .failure(let error):
7073
print("error: \(error)")
7174
}
72-
}
75+
})

Sources/APIKit/Concurrency/Concurrency.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public extension Session {
2323
return try await withTaskCancellationHandler(operation: {
2424
return try await withCheckedThrowingContinuation { continuation in
2525
Task {
26-
let sessionTask = createSessionTask(request, callbackQueue: callbackQueue, progressHandler: { _ in }) { result in
26+
let sessionTask = createSessionTask(request, callbackQueue: callbackQueue, uploadProgressHandler: { _ in }, downloadProgressHandler: { _ in }) { result in
2727
continuation.resume(with: result)
2828
}
2929
await cancellationHandler.register(with: sessionTask)

Sources/APIKit/Session.swift

+23-9
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ open class Session {
1010
/// The default callback queue for `send(_:handler:)`.
1111
public let callbackQueue: CallbackQueue
1212

13+
/// Closure type executed when the upload or download progress of a request.
14+
public typealias ProgressHandler = (Progress) -> Void
15+
1316
/// Returns `Session` instance that is initialized with `adapter`.
1417
/// - parameter adapter: The adapter that connects lower level backend with Session interface.
1518
/// - parameter callbackQueue: The default callback queue for `send(_:handler:)`.
@@ -33,11 +36,13 @@ open class Session {
3336
/// Calls `send(_:callbackQueue:handler:)` of `Session.shared`.
3437
/// - parameter request: The request to be sent.
3538
/// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used.
36-
/// - parameter handler: The closure that receives result of the request.
39+
/// - parameter uploadProgressHandler: The closure that receives upload progress of the request.
40+
/// - parameter downloadProgressHandler: The closure that receives download progress of the request.
41+
/// - parameter completionHandler: The closure that receives result of the request.
3742
/// - returns: The new session task.
3843
@discardableResult
39-
open class func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Progress) -> Void = { _ in }, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
40-
return shared.send(request, callbackQueue: callbackQueue, progressHandler: progressHandler, completionHandler: completionHandler)
44+
open class func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil, uploadProgressHandler: @escaping ProgressHandler = { _ in }, downloadProgressHandler: @escaping ProgressHandler = { _ in }, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
45+
return shared.send(request, callbackQueue: callbackQueue, uploadProgressHandler: uploadProgressHandler, downloadProgressHandler: downloadProgressHandler, completionHandler: completionHandler)
4146
}
4247

4348
/// Calls `cancelRequests(with:passingTest:)` of `Session.shared`.
@@ -51,11 +56,13 @@ open class Session {
5156
/// `Request.Response` is inferred from `Request` type parameter, the it changes depending on the request type.
5257
/// - parameter request: The request to be sent.
5358
/// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used.
54-
/// - parameter handler: The closure that receives result of the request.
59+
/// - parameter uploadProgressHandler: The closure that receives upload progress of the request.
60+
/// - parameter downloadProgressHandler: The closure that receives download progress of the request.
61+
/// - parameter completionHandler: The closure that receives result of the request.
5562
/// - returns: The new session task.
5663
@discardableResult
57-
open func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil, progressHandler: @escaping (Progress) -> Void = { _ in }, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
58-
let task = createSessionTask(request, callbackQueue: callbackQueue, progressHandler: progressHandler, completionHandler: completionHandler)
64+
open func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue? = nil, uploadProgressHandler: @escaping ProgressHandler = { _ in }, downloadProgressHandler: @escaping ProgressHandler = { _ in }, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
65+
let task = createSessionTask(request, callbackQueue: callbackQueue, uploadProgressHandler: uploadProgressHandler, downloadProgressHandler: downloadProgressHandler, completionHandler: completionHandler)
5966
task?.resume()
6067
return task
6168
}
@@ -77,7 +84,7 @@ open class Session {
7784
}
7885
}
7986

80-
internal func createSessionTask<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void) -> SessionTask? {
87+
internal func createSessionTask<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue?, uploadProgressHandler: @escaping ProgressHandler, downloadProgressHandler: @escaping ProgressHandler, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void) -> SessionTask? {
8188
let callbackQueue = callbackQueue ?? self.callbackQueue
8289
let urlRequest: URLRequest
8390
do {
@@ -90,8 +97,15 @@ open class Session {
9097
}
9198

9299
let task = adapter.createTask(with: urlRequest,
93-
progressHandler: { progress in
94-
progressHandler(progress)
100+
uploadProgressHandler: { progress in
101+
callbackQueue.execute {
102+
uploadProgressHandler(progress)
103+
}
104+
},
105+
downloadProgressHandler: { progress in
106+
callbackQueue.execute {
107+
downloadProgressHandler(progress)
108+
}
95109
},
96110
completionHandler: { data, urlResponse, error in
97111
let result: Result<Request.Response, SessionTaskError>

Sources/APIKit/SessionAdapter/SessionAdapter.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public protocol SessionTask: AnyObject {
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, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask
14+
func createTask(with URLRequest: URLRequest, uploadProgressHandler: @escaping Session.ProgressHandler, downloadProgressHandler: @escaping Session.ProgressHandler, 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

+31-12
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ extension URLSessionTask: SessionTask {
66

77
private var dataTaskResponseBufferKey = 0
88
private var taskAssociatedObjectCompletionHandlerKey = 0
9-
private var taskAssociatedObjectProgressHandlerKey = 0
9+
private var taskAssociatedObjectUploadProgressHandlerKey = 0
10+
private var taskAssociatedObjectDownloadProgressHandlerKey = 0
1011

1112
/// `URLSessionAdapter` connects `URLSession` with `Session`.
1213
///
@@ -26,12 +27,13 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS
2627
}
2728

2829
/// Creates `URLSessionDataTask` instance using `dataTaskWithRequest(_:completionHandler:)`.
29-
open func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask {
30+
open func createTask(with URLRequest: URLRequest, uploadProgressHandler: @escaping Session.ProgressHandler, downloadProgressHandler: @escaping Session.ProgressHandler, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask {
3031
let task = urlSession.dataTask(with: URLRequest)
3132

3233
setBuffer(NSMutableData(), forTask: task)
3334
setHandler(completionHandler, forTask: task)
34-
setProgressHandler(progressHandler, forTask: task)
35+
setUploadProgressHandler(uploadProgressHandler, forTask: task)
36+
setDownloadProgressHandler(downloadProgressHandler, forTask: task)
3537

3638
return task
3739
}
@@ -60,28 +62,45 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS
6062
return objc_getAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey) as? (Data?, URLResponse?, Error?) -> Void
6163
}
6264

63-
private func setProgressHandler(_ progressHandler: @escaping (Progress) -> Void, forTask task: URLSessionTask) {
64-
objc_setAssociatedObject(task, &taskAssociatedObjectProgressHandlerKey, progressHandler as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
65+
private func setUploadProgressHandler(_ progressHandler: @escaping Session.ProgressHandler, forTask task: URLSessionTask) {
66+
objc_setAssociatedObject(task, &taskAssociatedObjectUploadProgressHandlerKey, progressHandler as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
6567
}
6668

67-
private func progressHandler(for task: URLSessionTask) -> ((Progress) -> Void)? {
68-
return objc_getAssociatedObject(task, &taskAssociatedObjectProgressHandlerKey) as? (Progress) -> Void
69+
private func uploadProgressHandler(for task: URLSessionTask) -> Session.ProgressHandler? {
70+
return objc_getAssociatedObject(task, &taskAssociatedObjectUploadProgressHandlerKey) as? Session.ProgressHandler
71+
}
72+
73+
private func setDownloadProgressHandler(_ progressHandler: @escaping Session.ProgressHandler, forTask task: URLSessionTask) {
74+
objc_setAssociatedObject(task, &taskAssociatedObjectDownloadProgressHandlerKey, progressHandler as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
75+
}
76+
77+
private func downloadProgressHandler(for task: URLSessionTask) -> Session.ProgressHandler? {
78+
return objc_getAssociatedObject(task, &taskAssociatedObjectDownloadProgressHandlerKey) as? Session.ProgressHandler
6979
}
7080

7181
// MARK: URLSessionTaskDelegate
7282
open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
7383
handler(for: task)?(buffer(for: task) as Data?, task.response, error)
7484
}
7585

86+
open func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
87+
let progress = Progress(totalUnitCount: totalBytesExpectedToSend)
88+
progress.completedUnitCount = totalBytesSent
89+
uploadProgressHandler(for: task)?(progress)
90+
}
91+
7692
// MARK: URLSessionDataDelegate
7793
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
7894
buffer(for: dataTask)?.append(data)
95+
updateDownloadProgress(dataTask)
7996
}
8097

81-
// MARK: URLSessionDataDelegate
82-
open func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
83-
let progress = Progress(totalUnitCount: totalBytesExpectedToSend)
84-
progress.completedUnitCount = totalBytesSent
85-
progressHandler(for: task)?(progress)
98+
private func updateDownloadProgress(_ task: URLSessionTask) {
99+
let receivedData = buffer(for: task) as Data?
100+
let totalBytesReceived = Int64(receivedData?.count ?? 0)
101+
let totalBytesExpected = task.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown
102+
let progress = Progress(totalUnitCount: totalBytesExpected)
103+
progress.completedUnitCount = totalBytesReceived
104+
downloadProgressHandler(for: task)?(progress)
86105
}
87106
}

Tests/APIKitTests/SessionAdapterType/URLSessionAdapterSubclassTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class URLSessionAdapterSubclassTests: XCTestCase {
6464
let adapter = SessionAdapter(configuration: configuration)
6565
let session = Session(adapter: adapter)
6666

67-
session.send(request, progressHandler: { _ in
67+
session.send(request, uploadProgressHandler: { _ in
6868
expectation.fulfill()
6969
})
7070

Tests/APIKitTests/SessionTests.swift

+19-4
Original file line numberDiff line numberDiff line change
@@ -218,14 +218,29 @@ class SessionTests: XCTestCase {
218218
waitForExpectations(timeout: 1.0, handler: nil)
219219
}
220220

221-
func testProgress() {
221+
func testUploadProgress() {
222222
let dictionary = ["key": "value"]
223223
adapter.data = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
224224

225225
let expectation = self.expectation(description: "wait for response")
226226
let request = TestRequest(method: .post)
227227

228-
session.send(request, progressHandler: { progress in
228+
session.send(request, uploadProgressHandler: { progress in
229+
XCTAssertNotNil(progress)
230+
expectation.fulfill()
231+
})
232+
233+
waitForExpectations(timeout: 1.0, handler: nil)
234+
}
235+
236+
func testDownloadProgress() {
237+
let dictionary = ["key": "value"]
238+
adapter.data = try! JSONSerialization.data(withJSONObject: dictionary, options: [])
239+
240+
let expectation = self.expectation(description: "wait for response")
241+
let request = TestRequest(method: .post)
242+
243+
session.send(request, downloadProgressHandler: { progress in
229244
XCTAssertNotNil(progress)
230245
expectation.fulfill()
231246
})
@@ -248,7 +263,7 @@ class SessionTests: XCTestCase {
248263
return testSesssion
249264
}
250265

251-
override func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void) -> SessionTask? {
266+
override func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue?, uploadProgressHandler: @escaping Session.ProgressHandler, downloadProgressHandler: @escaping Session.ProgressHandler, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void) -> SessionTask? {
252267

253268
functionCallFlags[(#function)] = true
254269
return super.send(request)
@@ -263,7 +278,7 @@ class SessionTests: XCTestCase {
263278
SessionSubclass.send(TestRequest())
264279
SessionSubclass.cancelRequests(with: TestRequest.self)
265280

266-
XCTAssertEqual(testSession.functionCallFlags["send(_:callbackQueue:progressHandler:completionHandler:)"], true)
281+
XCTAssertEqual(testSession.functionCallFlags["send(_:callbackQueue:uploadProgressHandler:downloadProgressHandler:completionHandler:)"], true)
267282
XCTAssertEqual(testSession.functionCallFlags["cancelRequests(with:passingTest:)"], true)
268283
}
269284
}

Tests/APIKitTests/TestComponents/TestSessionAdapter.swift

+4-3
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,17 @@ class TestSessionAdapter: SessionAdapter {
4242
if task.cancelled {
4343
task.completionHandler(nil, nil, Error.cancelled)
4444
} else {
45-
task.progressHandler(Progress(totalUnitCount: 1))
45+
task.uploadProgressHandler(Progress(totalUnitCount: 1))
46+
task.downloadProgressHandler(Progress(totalUnitCount: 1))
4647
task.completionHandler(data, urlResponse, error)
4748
}
4849
}
4950

5051
tasks = []
5152
}
5253

53-
func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask {
54-
let task = TestSessionTask(progressHandler: progressHandler, completionHandler: completionHandler)
54+
func createTask(with URLRequest: URLRequest, uploadProgressHandler: @escaping Session.ProgressHandler, downloadProgressHandler: @escaping Session.ProgressHandler, completionHandler: @escaping (Data?, URLResponse?, Swift.Error?) -> Void) -> SessionTask {
55+
let task = TestSessionTask(uploadProgressHandler: uploadProgressHandler, downloadProgressHandler: downloadProgressHandler, completionHandler: completionHandler)
5556
tasks.append(task)
5657

5758
return task

Tests/APIKitTests/TestComponents/TestSessionTask.swift

+5-4
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@ import APIKit
44
class TestSessionTask: SessionTask {
55

66
var completionHandler: (Data?, URLResponse?, Error?) -> Void
7-
var progressHandler: (Progress) -> Void
7+
var uploadProgressHandler: Session.ProgressHandler
8+
var downloadProgressHandler: Session.ProgressHandler
89
var cancelled = false
910

10-
init(progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
11+
init(uploadProgressHandler: @escaping Session.ProgressHandler, downloadProgressHandler: @escaping Session.ProgressHandler, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
1112
self.completionHandler = completionHandler
12-
self.progressHandler = progressHandler
13+
self.uploadProgressHandler = uploadProgressHandler
14+
self.downloadProgressHandler = downloadProgressHandler
1315
}
1416

1517
func resume() {
16-
1718
}
1819

1920
func cancel() {

0 commit comments

Comments
 (0)