Skip to content

Commit

Permalink
Add a way to get progress of downloading.
Browse files Browse the repository at this point in the history
  • Loading branch information
Econa77 committed Oct 22, 2022
1 parent df0a67e commit ca9ed6a
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 38 deletions.
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

0 comments on commit ca9ed6a

Please sign in to comment.