Skip to content

Add a way to get progress of downloading. #308

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 1 commit into from
Oct 22, 2022
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
9 changes: 6 additions & 3 deletions Demo.playground/Contents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,16 @@ struct GetRateLimitRequest: GitHubRequest {
//: Step 4: Send request
let request = GetRateLimitRequest()

Session.send(request) { result in
Session.send(request, uploadProgressHandler: { progress in
print("upload progress: \(progress.fractionCompleted)")
}, downloadProgressHandler: { progress in
print("download progress: \(progress.fractionCompleted) %")
}, completionHandler: { result in
switch result {
case .success(let rateLimit):
print("count: \(rateLimit.count)")
print("reset: \(rateLimit.resetDate)")

case .failure(let error):
print("error: \(error)")
}
}
})
2 changes: 1 addition & 1 deletion Sources/APIKit/Concurrency/Concurrency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public extension Session {
return try await withTaskCancellationHandler(operation: {
return try await withCheckedThrowingContinuation { continuation in
Task {
let sessionTask = createSessionTask(request, callbackQueue: callbackQueue, progressHandler: { _ in }) { result in
let sessionTask = createSessionTask(request, callbackQueue: callbackQueue, uploadProgressHandler: { _ in }, downloadProgressHandler: { _ in }) { result in
continuation.resume(with: result)
}
await cancellationHandler.register(with: sessionTask)
Expand Down
32 changes: 23 additions & 9 deletions Sources/APIKit/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ open class Session {
/// The default callback queue for `send(_:handler:)`.
public let callbackQueue: CallbackQueue

/// Closure type executed when the upload or download progress of a request.
public typealias ProgressHandler = (Progress) -> Void

/// Returns `Session` instance that is initialized with `adapter`.
/// - parameter adapter: The adapter that connects lower level backend with Session interface.
/// - parameter callbackQueue: The default callback queue for `send(_:handler:)`.
Expand All @@ -33,11 +36,13 @@ open class Session {
/// Calls `send(_:callbackQueue:handler:)` of `Session.shared`.
/// - parameter request: The request to be sent.
/// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used.
/// - parameter handler: The closure that receives result of the request.
/// - parameter uploadProgressHandler: The closure that receives upload progress of the request.
/// - parameter downloadProgressHandler: The closure that receives download progress of the request.
/// - parameter completionHandler: 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, progressHandler: @escaping (Progress) -> Void = { _ in }, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
return shared.send(request, callbackQueue: callbackQueue, progressHandler: progressHandler, completionHandler: completionHandler)
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? {
return shared.send(request, callbackQueue: callbackQueue, uploadProgressHandler: uploadProgressHandler, downloadProgressHandler: downloadProgressHandler, completionHandler: completionHandler)
}

/// Calls `cancelRequests(with:passingTest:)` of `Session.shared`.
Expand All @@ -51,11 +56,13 @@ open class Session {
/// `Request.Response` is inferred from `Request` type parameter, the it changes depending on the request type.
/// - parameter request: The request to be sent.
/// - parameter callbackQueue: The queue where the handler runs. If this parameters is `nil`, default `callbackQueue` of `Session` will be used.
/// - parameter handler: The closure that receives result of the request.
/// - parameter uploadProgressHandler: The closure that receives upload progress of the request.
/// - parameter downloadProgressHandler: The closure that receives download progress of the request.
/// - parameter completionHandler: 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, progressHandler: @escaping (Progress) -> Void = { _ in }, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void = { _ in }) -> SessionTask? {
let task = createSessionTask(request, callbackQueue: callbackQueue, progressHandler: progressHandler, completionHandler: completionHandler)
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? {
let task = createSessionTask(request, callbackQueue: callbackQueue, uploadProgressHandler: uploadProgressHandler, downloadProgressHandler: downloadProgressHandler, completionHandler: completionHandler)
task?.resume()
return task
}
Expand All @@ -77,7 +84,7 @@ open class Session {
}
}

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

let task = adapter.createTask(with: urlRequest,
progressHandler: { progress in
progressHandler(progress)
uploadProgressHandler: { progress in
callbackQueue.execute {
uploadProgressHandler(progress)
}
},
downloadProgressHandler: { progress in
callbackQueue.execute {
downloadProgressHandler(progress)
}
},
completionHandler: { data, urlResponse, error in
let result: Result<Request.Response, SessionTaskError>
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: AnyObject {
/// with `Session`.
public protocol SessionAdapter {
/// Returns instance that conforms to `SessionTask`. `handler` must be called after success or failure.
func createTask(with URLRequest: URLRequest, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> SessionTask
func createTask(with URLRequest: URLRequest, uploadProgressHandler: @escaping Session.ProgressHandler, downloadProgressHandler: @escaping Session.ProgressHandler, 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
43 changes: 31 additions & 12 deletions Sources/APIKit/SessionAdapter/URLSessionAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ extension URLSessionTask: SessionTask {

private var dataTaskResponseBufferKey = 0
private var taskAssociatedObjectCompletionHandlerKey = 0
private var taskAssociatedObjectProgressHandlerKey = 0
private var taskAssociatedObjectUploadProgressHandlerKey = 0
private var taskAssociatedObjectDownloadProgressHandlerKey = 0

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

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

setBuffer(NSMutableData(), forTask: task)
setHandler(completionHandler, forTask: task)
setProgressHandler(progressHandler, forTask: task)
setUploadProgressHandler(uploadProgressHandler, forTask: task)
setDownloadProgressHandler(downloadProgressHandler, forTask: task)

return task
}
Expand Down Expand Up @@ -60,28 +62,45 @@ open class URLSessionAdapter: NSObject, SessionAdapter, URLSessionDelegate, URLS
return objc_getAssociatedObject(task, &taskAssociatedObjectCompletionHandlerKey) as? (Data?, URLResponse?, Error?) -> Void
}

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

private func progressHandler(for task: URLSessionTask) -> ((Progress) -> Void)? {
return objc_getAssociatedObject(task, &taskAssociatedObjectProgressHandlerKey) as? (Progress) -> Void
private func uploadProgressHandler(for task: URLSessionTask) -> Session.ProgressHandler? {
return objc_getAssociatedObject(task, &taskAssociatedObjectUploadProgressHandlerKey) as? Session.ProgressHandler
}

private func setDownloadProgressHandler(_ progressHandler: @escaping Session.ProgressHandler, forTask task: URLSessionTask) {
objc_setAssociatedObject(task, &taskAssociatedObjectDownloadProgressHandlerKey, progressHandler as Any, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}

private func downloadProgressHandler(for task: URLSessionTask) -> Session.ProgressHandler? {
return objc_getAssociatedObject(task, &taskAssociatedObjectDownloadProgressHandlerKey) as? Session.ProgressHandler
}

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

open func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
let progress = Progress(totalUnitCount: totalBytesExpectedToSend)
progress.completedUnitCount = totalBytesSent
uploadProgressHandler(for: task)?(progress)
}

// MARK: URLSessionDataDelegate
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
buffer(for: dataTask)?.append(data)
updateDownloadProgress(dataTask)
}

// MARK: URLSessionDataDelegate
open func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
let progress = Progress(totalUnitCount: totalBytesExpectedToSend)
progress.completedUnitCount = totalBytesSent
progressHandler(for: task)?(progress)
private func updateDownloadProgress(_ task: URLSessionTask) {
let receivedData = buffer(for: task) as Data?
let totalBytesReceived = Int64(receivedData?.count ?? 0)
let totalBytesExpected = task.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown
let progress = Progress(totalUnitCount: totalBytesExpected)
progress.completedUnitCount = totalBytesReceived
downloadProgressHandler(for: task)?(progress)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class URLSessionAdapterSubclassTests: XCTestCase {
let adapter = SessionAdapter(configuration: configuration)
let session = Session(adapter: adapter)

session.send(request, progressHandler: { _ in
session.send(request, uploadProgressHandler: { _ in
expectation.fulfill()
})

Expand Down
23 changes: 19 additions & 4 deletions Tests/APIKitTests/SessionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,14 +218,29 @@ class SessionTests: XCTestCase {
waitForExpectations(timeout: 1.0, handler: nil)
}

func testProgress() {
func testUploadProgress() {
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: { progress in
session.send(request, uploadProgressHandler: { progress in
XCTAssertNotNil(progress)
expectation.fulfill()
})

waitForExpectations(timeout: 1.0, handler: nil)
}

func testDownloadProgress() {
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, downloadProgressHandler: { progress in
XCTAssertNotNil(progress)
expectation.fulfill()
})
Expand All @@ -248,7 +263,7 @@ class SessionTests: XCTestCase {
return testSesssion
}

override func send<Request: APIKit.Request>(_ request: Request, callbackQueue: CallbackQueue?, progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Result<Request.Response, SessionTaskError>) -> Void) -> SessionTask? {
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? {

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

XCTAssertEqual(testSession.functionCallFlags["send(_:callbackQueue:progressHandler:completionHandler:)"], true)
XCTAssertEqual(testSession.functionCallFlags["send(_:callbackQueue:uploadProgressHandler:downloadProgressHandler:completionHandler:)"], true)
XCTAssertEqual(testSession.functionCallFlags["cancelRequests(with:passingTest:)"], true)
}
}
7 changes: 4 additions & 3 deletions Tests/APIKitTests/TestComponents/TestSessionAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,17 @@ class TestSessionAdapter: SessionAdapter {
if task.cancelled {
task.completionHandler(nil, nil, Error.cancelled)
} else {
task.progressHandler(Progress(totalUnitCount: 1))
task.uploadProgressHandler(Progress(totalUnitCount: 1))
task.downloadProgressHandler(Progress(totalUnitCount: 1))
task.completionHandler(data, urlResponse, error)
}
}

tasks = []
}

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

return task
Expand Down
9 changes: 5 additions & 4 deletions Tests/APIKitTests/TestComponents/TestSessionTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ import APIKit
class TestSessionTask: SessionTask {

var completionHandler: (Data?, URLResponse?, Error?) -> Void
var progressHandler: (Progress) -> Void
var uploadProgressHandler: Session.ProgressHandler
var downloadProgressHandler: Session.ProgressHandler
var cancelled = false

init(progressHandler: @escaping (Progress) -> Void, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
init(uploadProgressHandler: @escaping Session.ProgressHandler, downloadProgressHandler: @escaping Session.ProgressHandler, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
self.completionHandler = completionHandler
self.progressHandler = progressHandler
self.uploadProgressHandler = uploadProgressHandler
self.downloadProgressHandler = downloadProgressHandler
}

func resume() {

}

func cancel() {
Expand Down