diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 305dc1d..38d5042 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,11 +8,14 @@ on: jobs: release: name: Make CocoaPods release - runs-on: macOS-latest + runs-on: macos-11 steps: - name: Checkout uses: actions/checkout@v2 + - name: Change Xcode + run: sudo xcode-select -s /Applications/Xcode_13.2.1.app + - name: Install Cocoapods run: gem install cocoapods diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e7b80dc..2890a51 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,9 +14,11 @@ on: jobs: test: name: Run tests - runs-on: macOS-latest + runs-on: macos-11 steps: - name: Checkout uses: actions/checkout@v2 + - name: Change Xcode + run: sudo xcode-select -s /Applications/Xcode_13.2.1.app - name: Build and test run: swift test --enable-code-coverage --disable-automatic-resolution \ No newline at end of file diff --git a/Sources/Apexy/Client.swift b/Sources/Apexy/Client.swift index 54e8c5e..cdd405e 100644 --- a/Sources/Apexy/Client.swift +++ b/Sources/Apexy/Client.swift @@ -28,12 +28,12 @@ public protocol Client: AnyObject { /// Send request to specified endpoint. /// - Returns: response data from the server for the request. - @available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) func request(_ endpoint: T) async throws -> T.Content where T: Endpoint /// Upload data to specified endpoint. /// - Returns: response data from the server for the upload. - @available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) func upload(_ endpoint: T) async throws -> T.Content where T: UploadEndpoint } diff --git a/Sources/Apexy/ProgressWrapper.swift b/Sources/Apexy/ProgressWrapper.swift new file mode 100644 index 0000000..9f31ba1 --- /dev/null +++ b/Sources/Apexy/ProgressWrapper.swift @@ -0,0 +1,36 @@ +// +// ProgressWrapper.swift +// ApexyURLSession +// +// Created by Aleksei Tiurnin on 20.01.2022. +// + +import Foundation + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) +public final class ProgressWrapper { + + public var progress: Progress? { + get { + lock.lock() + defer { lock.unlock() } + return _progress + } + set { + lock.lock() + defer { lock.unlock() } + _progress = newValue + } + } + + private var _progress: Progress? + private let lock = NSLock() + + public init(_progress: Progress? = nil) { + self._progress = _progress + } + + public func cancel() { + progress?.cancel() + } +} diff --git a/Sources/ApexyAlamofire/AlamofireClient.swift b/Sources/ApexyAlamofire/AlamofireClient.swift index 18b441a..2b88dc0 100755 --- a/Sources/ApexyAlamofire/AlamofireClient.swift +++ b/Sources/ApexyAlamofire/AlamofireClient.swift @@ -5,6 +5,7 @@ // Copyright © 2019 RedMadRobot. All rights reserved. // +import Apexy import Foundation import Alamofire @@ -179,7 +180,7 @@ open class AlamofireClient: Client { return progress } - @available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) public func request(_ endpoint: T) async throws -> T.Content where T : Endpoint { typealias ContentContinuation = CheckedContinuation let progressWrapper = ProgressWrapper() @@ -192,7 +193,7 @@ open class AlamofireClient: Client { }) } - @available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) public func upload(_ endpoint: T) async throws -> T.Content where T : UploadEndpoint { typealias ContentContinuation = CheckedContinuation let progressWrapper = ProgressWrapper() @@ -246,27 +247,3 @@ public extension Error { return self } } - -@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) -private final class ProgressWrapper { - - var progress: Progress? { - get { - lock.lock() - defer { lock.unlock() } - return _progress - } - set { - lock.lock() - defer { lock.unlock() } - _progress = newValue - } - } - - private var _progress: Progress? - private let lock = NSLock() - - func cancel() { - progress?.cancel() - } -} diff --git a/Sources/ApexyURLSession/URLSession+AsyncAwait13.swift b/Sources/ApexyURLSession/URLSession+AsyncAwait13.swift new file mode 100644 index 0000000..f95b5be --- /dev/null +++ b/Sources/ApexyURLSession/URLSession+AsyncAwait13.swift @@ -0,0 +1,77 @@ +// +// URLSession+AsyncAwait13.swift +// ApexyURLSession +// +// Created by Aleksei Tiurnin on 20.01.2022. +// + +import Apexy +import Foundation + +@available(macOS, introduced: 10.15, deprecated: 12, message: "Extension is no longer necessary. Use API built into SDK") +@available(iOS, introduced: 13, deprecated: 15, message: "Extension is no longer necessary. Use API built into SDK") +@available(watchOS, introduced: 6, deprecated: 8, message: "Extension is no longer necessary. Use API built into SDK") +@available(tvOS, introduced: 13, deprecated: 15, message: "Extension is no longer necessary. Use API built into SDK") +extension URLSession { + + typealias ContentContinuation = CheckedContinuation<(Data, URLResponse), Error> + + private func adaptToAsync( + dataTaskClosure: (ContentContinuation) -> URLSessionDataTask) async throws -> (Data, URLResponse) { + let progressWrapper = ProgressWrapper() + return try await withTaskCancellationHandler(handler: { + progressWrapper.cancel() + }, operation: { + try await withCheckedThrowingContinuation { (continuation: ContentContinuation) in + let task = dataTaskClosure(continuation) + progressWrapper.progress = task.progress + + task.resume() + } + }) + } + + public func data( + for request: URLRequest, + delegate: URLSessionTaskDelegate? = nil) async throws -> (Data, URLResponse) { + return try await adaptToAsync(dataTaskClosure: { continuation in + return self.dataTask(with: request) { data, response, error in + guard let data = data, let response = response else { + let error = error ?? URLError(.badServerResponse) + return continuation.resume(throwing: error) + } + continuation.resume(returning: (data, response)) + } + }) + } + + public func upload( + for request: URLRequest, + fromFile fileURL: URL, + delegate: URLSessionTaskDelegate? = nil) async throws -> (Data, URLResponse) { + return try await adaptToAsync(dataTaskClosure: { continuation in + return self.uploadTask(with: request, fromFile: fileURL) { data, response, error in + guard let data = data, let response = response else { + let error = error ?? URLError(.badServerResponse) + return continuation.resume(throwing: error) + } + continuation.resume(returning: (data, response)) + } + }) + } + + public func upload( + for request: URLRequest, + from bodyData: Data, + delegate: URLSessionTaskDelegate? = nil) async throws -> (Data, URLResponse) { + return try await adaptToAsync(dataTaskClosure: { continuation in + return self.uploadTask(with: request, from: bodyData) { data, response, error in + guard let data = data, let response = response else { + let error = error ?? URLError(.badServerResponse) + return continuation.resume(throwing: error) + } + continuation.resume(returning: (data, response)) + } + }) + } +} diff --git a/Sources/ApexyURLSession/URLSessionClient.swift b/Sources/ApexyURLSession/URLSessionClient.swift index 18f79a5..2c175c7 100644 --- a/Sources/ApexyURLSession/URLSessionClient.swift +++ b/Sources/ApexyURLSession/URLSessionClient.swift @@ -1,3 +1,4 @@ +import Apexy import Foundation open class URLSessionClient: Client { @@ -127,7 +128,7 @@ open class URLSessionClient: Client { return task.progress } - @available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) open func request(_ endpoint: T) async throws -> T.Content where T : Endpoint { var request = try endpoint.makeRequest() request = try requestAdapter.adapt(request) @@ -156,7 +157,7 @@ open class URLSessionClient: Client { } } - @available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) open func upload(_ endpoint: T) async throws -> T.Content where T : UploadEndpoint { var request: (request: URLRequest, body: UploadEndpointBody) = try endpoint.makeRequest() request.request = try requestAdapter.adapt(request.request) diff --git a/Tests/ApexyURLSessionTests/URLSessionClientTests.swift b/Tests/ApexyURLSessionTests/URLSessionClientTests.swift index 7bbcd12..33624be 100644 --- a/Tests/ApexyURLSessionTests/URLSessionClientTests.swift +++ b/Tests/ApexyURLSessionTests/URLSessionClientTests.swift @@ -119,6 +119,42 @@ final class URLSessionClientTests: XCTestCase { wait(for: [exp], timeout: 0.1) } + + @available(iOS 13.0, *) + @available(macOS 10.15, *) + func testClientDataRequestUsingAsyncAwait() async throws { + let endpoint = EmptyEndpoint() + let data = "Test".data(using: .utf8)! + MockURLProtocol.requestHandler = { request in + let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "2.0", headerFields: nil)! + return (response, data) + } + + do { + let content = try await client.request(endpoint) + XCTAssertEqual(content, data) + } catch { + XCTFail("Expected result: .success, actual result: .failure") + } + } + + @available(iOS 13.0, *) + @available(macOS 10.15, *) + func testClientUploadUsingAsyncAwait() async throws { + let data = "apple".data(using: .utf8)! + let endpoint = SimpleUploadEndpoint(data: data) + MockURLProtocol.requestHandler = { request in + let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "2.0", headerFields: nil)! + return (response, data) + } + + do { + let content = try await client.upload(endpoint) + XCTAssertEqual(content, data) + } catch { + XCTFail("Expected result: .success, actual result: .failure") + } + } } private final class MockURLProtocol: URLProtocol {