Skip to content

Commit

Permalink
Add download progress functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
rocxteady committed May 9, 2024
1 parent 1789053 commit 2730c9f
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 67 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "Resting",
defaultLocalization: "en",
platforms: [.iOS(.v13), .macOS(.v10_15)],
platforms: [.iOS(.v15), .macOS(.v12), .watchOS(.v8), .tvOS(.v15)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
Expand Down
34 changes: 0 additions & 34 deletions Sources/Resting/Helpers/URLSession+AsyncDownload.swift

This file was deleted.

65 changes: 58 additions & 7 deletions Sources/Resting/Resting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,21 @@ public struct RestClientConfiguration {
/// Represents a client for making RESTful network requests.
/// ///
/// This client utilizes a `RestClientConfiguration` to configure its behavior, including the session configuration and JSON decoding.
public class RestClient {
private let session: URLSession
public class RestClient: NSObject {
private lazy var session: URLSession = URLSession(configuration: clientConfiguration.sessionConfiguration, delegate: self, delegateQueue: nil)
private let clientConfiguration: RestClientConfiguration

private var downloadCompletion: ((URL?, Error?) -> Void)?
private var progress: ((Double) -> Void)?
private var downloadTask: URLSessionDownloadTask?

/// Creates a new `RestClient` instance with the specified `RestClientConfiguration`.
///
/// - Parameters:
/// - configuration: The configuration used to set up the client, defaults to a new `RestClientConfiguration` instance with a default `URLSessionConfiguration`.
public init(configuration: RestClientConfiguration = .init(sessionConfiguration: .default)) {
self.clientConfiguration = configuration
self.session = URLSession(configuration: configuration.sessionConfiguration)
super.init()
}
}

Expand Down Expand Up @@ -68,10 +72,57 @@ extension RestClient {
/// - Parameter configuration: The configuration for the network request.
/// - Returns: A `URL` to the downloaded file.
/// - Throws: Throws an error if the request fails or if the response can't be created.
public func download(with configuration: RequestConfiguration) async throws -> URL {
let urlRequest = try configuration.createURLRequest()
let response = try await session.asyncDownload(for: urlRequest)
return response.0
public func download(with configuration: RequestConfiguration, completion: @escaping (URL?, Error?) -> Void, progress: ((Double) -> Void)? = nil) {
self.downloadCompletion = completion
self.progress = progress
do {
let urlRequest = try configuration.createURLRequest()
downloadTask = session.downloadTask(with: urlRequest)
downloadTask?.resume()
} catch {
completion(nil, error)
self.downloadCompletion = nil
self.progress = nil
}
}

public func cancel() {
downloadTask?.cancel()
}
}

extension RestClient: URLSessionDownloadDelegate {
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) {
downloadCompletion?(nil, error)
downloadCompletion = nil
progress = nil
}

public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
defer {
self.downloadCompletion = nil
self.progress = nil
}
do {
let documentsURL = try
FileManager.default.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
let savedURL = documentsURL.appendingPathComponent(
location.lastPathComponent)
try FileManager.default.moveItem(at: location, to: savedURL)
downloadCompletion?(savedURL, nil)
} catch {
downloadCompletion?(nil, error)
}
}

public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
guard downloadTask == self.downloadTask else { return }
let bytesWritten = Double(bytesWritten)
let totalBytesWritten = Double(totalBytesWritten)
progress?(bytesWritten/totalBytesWritten)
}
}

Expand Down
1 change: 1 addition & 0 deletions Tests/RestingTests/RequestConfigurationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ final class RequestConfigurationTests: XCTestCase {
}
}


func testCreatingURLConfigurationWithParameterTypeFailure() throws {
let requestConfiguration: RequestConfiguration = .init(urlString: "http://www.example.com", method: .get, body: Data())
do {
Expand Down
134 changes: 114 additions & 20 deletions Tests/RestingTests/RestClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Combine
final class RestClientTests: XCTestCase {
private let configuration = URLSessionConfiguration.default
private var cancellables = Set<AnyCancellable>()
var xClient: RestClient?

override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
Expand Down Expand Up @@ -60,7 +61,7 @@ final class RestClientTests: XCTestCase {
XCTAssertEqual(response.title, "Title Example", "Response data don't match the example data!")
}

func testDownloadAsyncAwait() async throws {
func testDownloadAsyncAwait() {
let exampleString = "Text"
let exampleData = exampleString.data(using: .utf8)
MockedURLService.observer = { request -> (URLResponse?, Data?) in
Expand All @@ -70,10 +71,94 @@ final class RestClientTests: XCTestCase {
let restClient = RestClient(configuration: .init(sessionConfiguration: configuration))
let configuration = RequestConfiguration(urlString: "http://www.example.com", parameters: nil)

let responseURL: URL = try await restClient.download(with: configuration)
let responseData = try Data(contentsOf: responseURL)
let expectation = XCTestExpectation(description: "Downloading file...")

XCTAssertEqual(responseData, exampleData, "Downloaded file don't match the example file data!")
restClient.download(with: configuration) { url, error in
guard let url else {
XCTFail("Downloaded file url should not be nil!")
return
}
guard error == nil else {
XCTFail("Download shouldn't have been failed!")
return
}
do {
let responseData = try Data(contentsOf: url)
XCTAssertEqual(responseData, exampleData, "Downloaded file don't match the example file data!")
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
}

wait(for: [expectation], timeout: 1.0)
}

func testDownloadWithProgressAsyncAwait() {
let exampleString = "Text"
let exampleData = exampleString.data(using: .utf8)
MockedURLService.observer = { request -> (URLResponse?, Data?) in
let response = HTTPURLResponse(url: URL(string: "http://www.example.com")!, statusCode: 200, httpVersion: nil, headerFields: nil)
return (response, exampleData)
}
let restClient = RestClient(configuration: .init(sessionConfiguration: configuration))
let configuration = RequestConfiguration(urlString: "http://www.example.com", parameters: nil)

let expectation = XCTestExpectation(description: "Downloading file...")

var currentProgress: Double = 0

restClient.download(with: configuration) { url, error in
guard let url else {
XCTFail("Downloaded file url should not be nil!")
return
}
guard error == nil else {
XCTFail("Download shouldn't have been failed!")
return
}
do {
let responseData = try Data(contentsOf: url)
XCTAssertEqual(responseData, exampleData, "Downloaded file don't match the example file data!")
XCTAssertEqual(currentProgress, 1, "Downloaded progress don't match!")
} catch {
XCTFail(error.localizedDescription)
}
expectation.fulfill()
} progress: { progress in
currentProgress = progress
}

wait(for: [expectation], timeout: 1.0)
}

func testCancellingDownloadAsyncAwait() {
let exampleString = "Text"
let exampleData = exampleString.data(using: .utf8)
MockedURLService.observer = { request -> (URLResponse?, Data?) in
let response = HTTPURLResponse(url: URL(string: "http://www.example.com")!, statusCode: 200, httpVersion: nil, headerFields: nil)
return (response, exampleData)
}
let restClient = RestClient(configuration: .init(sessionConfiguration: configuration))
let configuration = RequestConfiguration(urlString: "http://www.example.com", parameters: nil)

let expectation = XCTestExpectation(description: "Downloading file...")

restClient.download(with: configuration) { url, error in
guard let error else {
XCTFail("Download should have been failed!")
return
}
guard let error = error as? URLError,
error.code == .cancelled else {
XCTFail("Download should have been failed as cancelled!")
return
}
expectation.fulfill()
}
restClient.cancel()

wait(for: [expectation], timeout: 1.0)
}

func testPublisher() throws {
Expand Down Expand Up @@ -126,22 +211,6 @@ final class RestClientTests: XCTestCase {
}
}

func testAsyncAwaitWithUnknownFailure() async throws {
MockedURLService.observer = { request -> (URLResponse?, Data?) in
return (nil, nil)
}
let restClient = RestClient(configuration: .init(sessionConfiguration: configuration))
let configuration = RequestConfiguration(urlString: "http://www.example.com", method: .get)

do {
let _: URL = try await restClient.download(with: configuration)
XCTFail("Downloading should have been failed with unknown case!")
} catch RestingError.unknown {
} catch {
XCTFail(error.localizedDescription)
}
}

func testPublisherWithPublisherFailure() throws {
MockedURLService.observer = { request -> (URLResponse?, Data?) in
let response = HTTPURLResponse(url: URL(string: "http://\u{FFFD}\u{FFFE}")!, statusCode: 200, httpVersion: nil, headerFields: nil)
Expand Down Expand Up @@ -170,4 +239,29 @@ final class RestClientTests: XCTestCase {

waitForExpectations(timeout: 1)
}

func testHandlingURLConfigurationFailureWithDownload() {
MockedURLService.observer = { request -> (URLResponse?, Data?) in
let response = HTTPURLResponse(url: URL(string: "http://\u{FFFD}\u{FFFE}")!, statusCode: 403, httpVersion: nil, headerFields: nil)
return (response, nil)
}
let restClient = RestClient(configuration: .init(sessionConfiguration: configuration))
let configuration = RequestConfiguration(urlString: "http://\u{FFFD}\u{FFFE}", method: .get)

let expectation = XCTestExpectation(description: "Downloading file...")

restClient.download(with: configuration) { url, error in
guard url == nil else {
XCTFail("Download file url should be nil!")
return
}
guard error != nil else {
XCTFail("Download should not be nil!")
return
}
expectation.fulfill()
}

wait(for: [expectation], timeout: 1.0)
}
}
20 changes: 15 additions & 5 deletions Tests/RestingTests/RestClientWithFailureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,28 @@ final class RestClientWithFailureTests: XCTestCase {
waitForExpectations(timeout: 1)
}

func testAsyncAwaitWithFailure() async throws {
func testAsyncAwaitWithFailure() {
MockedURLService.observer = { request -> (URLResponse?, Data?) in
let response = HTTPURLResponse(url: URL(string: "unsupported_url")!, statusCode: 403, httpVersion: nil, headerFields: nil)
return (response, nil)
}
let restClient = RestClient(configuration: .init(sessionConfiguration: configuration))
let configuration = RequestConfiguration(urlString: "unsupported_url", method: .get)

do {
let url: URL = try await restClient.download(with: configuration)
XCTFail("Downloading should have been failed!")
} catch {
let expectation = XCTestExpectation(description: "Downloading file...")

restClient.download(with: configuration) { url, error in
guard url == nil else {
XCTFail("Download file url should be nil!")
return
}
guard error != nil else {
XCTFail("Download should have been failed!")
return
}
expectation.fulfill()
}

wait(for: [expectation], timeout: 1.0)
}
}

0 comments on commit 2730c9f

Please sign in to comment.