Skip to content

Commit

Permalink
Issue 18 - Adding Upload Completion Handler for Handling Custom/Error… (
Browse files Browse the repository at this point in the history
#19)

* Issue 18 - Adding Upload Completion Handler for Handling Custom/Error Responses

* [Issue 18] Update Register APIs and Documentation for Uploads

* [Issue 18] Update Upload Unit Tests
  • Loading branch information
matt-certain-ct authored Sep 24, 2024
1 parent 8093c39 commit 840a537
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation

// MARK: - URLSessionTaskDelegate

extension NetworkManager: URLSessionTaskDelegate {
extension NetworkManager: URLSessionTaskDelegate, URLSessionDataDelegate {
/// :nodoc:
public func urlSession(
_ session: URLSession,
Expand Down Expand Up @@ -40,4 +40,15 @@ extension NetworkManager: URLSessionTaskDelegate {
fileUpload.urlSession(session, task: task, didCompleteWithError: error)
}
}

/// :nodoc:
public func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive data: Data
) {
if dataTask is URLSessionUploadTask {
fileUpload.urlSession(session, dataTask: dataTask, didReceive: data)
}
}
}
6 changes: 4 additions & 2 deletions Sources/YNetwork/NetworkManager/NetworkManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,12 @@ open class NetworkManager: NSObject {
/// - Parameters:
/// - request: the upload network request to submit
/// - progress: progress handler (will be called back on main thread)
/// - completionHandler: file upload handler (will be called back on URLSession background thread).
/// - Returns: a cancelable upload task if one was able to be created, otherwise nil if no task was issued
@discardableResult open func submitBackgroundUpload(
_ request: NetworkRequest,
progress: ProgressHandler? = nil
progress: ProgressHandler? = nil,
completionHandler: FileUploadHandler? = nil
) -> Cancelable? {
guard let urlRequest = try? buildUrlRequest(request: request) else { return nil }

Expand All @@ -178,7 +180,7 @@ open class NetworkManager: NSObject {

// creating the upload task copies the file
let task = try? configuration?.networkEngine.submitBackgroundUpload(urlRequest, fileUrl: localURL)
fileUpload.register(task, progress: progress)
fileUpload.registerUpload(task, progress: progress, completion: completionHandler)
return task
}

Expand Down
61 changes: 58 additions & 3 deletions Sources/YNetwork/NetworkManager/Progress/FileProgress.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ public typealias Percentage = Double
/// Guaranteed to be called back on the main thread
public typealias ProgressHandler = (Percentage) -> Void

/// Asynchronous completion handler that is called when a response is received for an upload
public typealias FileUploadHandler = (Data) -> Void

/// Asynchronous cancellation handler that is called when an upload request is cancelled
/// This can be used with the cancellationHandler attribute of the Progress object associated with the upload task
public typealias CancellationHandler = () -> Void

/// Asynchronous completion handler that reports the status of request.
///
/// Guaranteed to be called back on a background thread (because the system erases the temporary file)
Expand All @@ -29,6 +36,7 @@ public typealias FileDownloadHandler = (Result<URL, Error>) -> Void
internal class FileProgress: NSObject {
/// Stores the progress handlers to be called, keyed by unique task identifier
private var progressHandlersByTaskID: [Int: ProgressHandler] = [:]
private var uploadHandlerByTaskID: [Int: FileUploadHandler] = [:]
private var downloadHandlersByTaskID: [Int: FileDownloadHandler] = [:]

/// Updates the progress handler for the specified task with the percentage value
Expand All @@ -41,6 +49,17 @@ internal class FileProgress: NSObject {
progressHandler(percent)
}
}

/// Invokes the completion handler for the specified task with the response data
/// - Parameters:
/// - data: the response data that can be decoded for custom responses such as error messages
/// - taskIdentifier: unique task identifier
func receive(data: Data, forKey taskIdentifier: Int) {
guard let completionHandler = uploadHandlerByTaskID[taskIdentifier] else { return }
DispatchQueue.main.async {
completionHandler(data)
}
}

/// Updates the request status for the specified task with the file URL
/// - Parameters:
Expand All @@ -53,16 +72,32 @@ internal class FileProgress: NSObject {
completionhandler(result)
}

/// Registers a data task for file progress.
/// Registers a data task for file progress of either an upload or download.
/// - Parameters:
/// - cancelable: optional cancelable task
/// - progress: optional progress handler
func register(_ cancelable: Cancelable?, progress: ProgressHandler?) {
func registerProgress(
_ cancelable: Cancelable?,
progress: ProgressHandler?
) {
guard let task = cancelable as? URLSessionTask,
let progress = progress else { return }
progressHandlersByTaskID[task.taskIdentifier] = progress
}

/// Registers the data task with a completion handler to be called when the response to the upload is received.
/// - Parameters:
/// - cancelable: optional cancelable task
/// - completion: optional completion handler
func registerCompletion(
_ cancelable: Cancelable?,
completion: FileUploadHandler?
) {
guard let task = cancelable as? URLSessionTask,
let completion = completion else { return }
uploadHandlerByTaskID[task.taskIdentifier] = completion
}

/// Registers a data task for file progress.
/// - Parameters:
/// - cancelable: optional cancelable task
Expand All @@ -73,17 +108,37 @@ internal class FileProgress: NSObject {
progress: ProgressHandler?,
handler: @escaping FileDownloadHandler
) {
register(cancelable, progress: progress)
registerProgress(cancelable, progress: progress)
guard let task = cancelable as? URLSessionTask else { return }
downloadHandlersByTaskID[task.taskIdentifier] = handler
}

/// Registers a data task for file upload progress and completion.
/// - Parameters:
/// - cancelable: optional cancelable task
/// - progress: optional progress handler
/// - completion: optional completion handler
func registerUpload(
_ cancelable: Cancelable?,
progress: ProgressHandler?,
completion: FileUploadHandler?
) {
registerProgress(cancelable, progress: progress)
registerCompletion(cancelable, completion: completion)
}

/// Unregisters a data task for file progress
/// - Parameter taskIdentifier: unique task identifier
func unregister(forKey taskIdentifier: Int) {
progressHandlersByTaskID.removeValue(forKey: taskIdentifier)
downloadHandlersByTaskID.removeValue(forKey: taskIdentifier)
}

/// Unregisters a completion handler, should be called once the final response is received
/// - Parameter taskIdentifier: unique task identifier
func unregisterUploadCompletion(forKey taskIdentifier: Int) {
uploadHandlerByTaskID.removeValue(forKey: taskIdentifier)
}

func checkResponseForError(task: URLSessionTask) -> Error? {
guard let httpResponse = task.response as? HTTPURLResponse else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import Foundation
/// to optionally track progress for large file upload tasks.
internal class FileUploadProgress: FileProgress { }

extension FileUploadProgress: URLSessionTaskDelegate {
extension FileUploadProgress: URLSessionTaskDelegate, URLSessionDataDelegate {
func urlSession(
_ session: URLSession,
task: URLSessionTask,
Expand All @@ -31,4 +31,14 @@ extension FileUploadProgress: URLSessionTaskDelegate {
// clean up the task now that we're finished with it
unregister(forKey: task.taskIdentifier)
}

public func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive data: Data
) {
// clean up the task now that the final response for the upload was received
receive(data: data, forKey: dataTask.taskIdentifier)
unregisterUploadCompletion(forKey: dataTask.taskIdentifier)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ final class NetworkManagerUploadTests: XCTestCase {

XCTAssertNil(sut.receivedError)

let task = try XCTUnwrap(sut.submitBackgroundUpload(request) { _ in } as? URLSessionTask)
let task = try XCTUnwrap(sut.submitBackgroundUpload(request, completionHandler: { _ in }) as? URLSessionTask)

Check failure on line 77 in Tests/YNetworkTests/NetworkManager/NetworkManagerUploadTests.swift

View workflow job for this annotation

GitHub Actions / build

Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon)

Check failure on line 77 in Tests/YNetworkTests/NetworkManager/NetworkManagerUploadTests.swift

View workflow job for this annotation

GitHub Actions / build

Opening braces should be preceded by a single space and on the same line as the declaration (opening_brace)
task.cancel() // this will make it fail
task.resume() // resume it

Expand All @@ -90,7 +90,7 @@ final class NetworkManagerUploadTests: XCTestCase {
XCTAssertNil(sut.receivedError)

URLProtocolStub.appendStub(.failure(NetworkError.invalidResponse), type: .upload)
let task = sut.submitBackgroundUpload(request) { _ in }
let task = sut.submitBackgroundUpload(request, completionHandler: { _ in })

Check failure on line 93 in Tests/YNetworkTests/NetworkManager/NetworkManagerUploadTests.swift

View workflow job for this annotation

GitHub Actions / build

Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon)

Check failure on line 93 in Tests/YNetworkTests/NetworkManager/NetworkManagerUploadTests.swift

View workflow job for this annotation

GitHub Actions / build

Opening braces should be preceded by a single space and on the same line as the declaration (opening_brace)

XCTAssertNotNil(task)

Expand Down Expand Up @@ -154,7 +154,7 @@ final class NetworkManagerUploadTests: XCTestCase {
let sut = NetworkManager()

// Given we submit a request without first configuring the network manager
let task = sut.submitBackgroundUpload(request) { _ in }
let task = sut.submitBackgroundUpload(request, completionHandler: { _ in })

Check failure on line 157 in Tests/YNetworkTests/NetworkManager/NetworkManagerUploadTests.swift

View workflow job for this annotation

GitHub Actions / build

Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon)

Check failure on line 157 in Tests/YNetworkTests/NetworkManager/NetworkManagerUploadTests.swift

View workflow job for this annotation

GitHub Actions / build

Opening braces should be preceded by a single space and on the same line as the declaration (opening_brace)

// We don't expect a task to be returned
XCTAssertNil(task)
Expand Down Expand Up @@ -209,19 +209,17 @@ private final class NetworkManagerSpy: NetworkManager {
self.fulfill()
}
}

override func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.receivedData = data
}

func fulfill() {
expectation?.fulfill()
expectation = nil
}
}

extension NetworkManagerSpy: URLSessionDataDelegate {
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.receivedData = data
}
}

public enum NetworkSpyError: Error {
case cancelled
}

0 comments on commit 840a537

Please sign in to comment.