From c7d75f8c39c8ee92a7069669604c5ee0cb46be0f Mon Sep 17 00:00:00 2001 From: Ivan Vavilov Date: Fri, 19 Jan 2024 08:21:59 -0800 Subject: [PATCH] Add session delegate to initializer (#44) * Add session delegate to initializer * Change method signature for data * update podfile.lock * Update github action for tests * update for min iOS 13 - remove URLSession extension - remove AsyncAwaitHelper - remove ProgressWrapper - remove AsyncAwaitHelper tests * update macos runner version --------- Co-authored-by: Aleksei Tiurnin --- .github/workflows/tests.yml | 6 +- Apexy.podspec | 14 ++-- Example/Example.xcodeproj/project.pbxproj | 6 +- .../Business Logic/Service/BookService.swift | 15 +--- .../Business Logic/Service/FileService.swift | 28 +------ .../Sources/Business Logic/ServiceLayer.swift | 14 ++-- .../Sources/Presentation/ViewController.swift | 59 +------------ Example/Podfile.lock | 16 ++-- Package.swift | 8 +- README.md | 2 +- README.ru.md | 2 +- .../Helpers/AsyncAwaitHelper.swift | 24 ------ .../Helpers/ProgressWrapper.swift | 39 --------- .../Helpers/URLSession+Concurrency.swift | 83 ------------------- .../ApexyURLSession/URLSessionClient.swift | 7 +- .../AsyncAwaitHelperTests.swift | 40 --------- 16 files changed, 45 insertions(+), 318 deletions(-) delete mode 100644 Sources/ApexyURLSession/Helpers/AsyncAwaitHelper.swift delete mode 100644 Sources/ApexyURLSession/Helpers/ProgressWrapper.swift delete mode 100644 Sources/ApexyURLSession/Helpers/URLSession+Concurrency.swift delete mode 100644 Tests/ApexyURLSessionTests/AsyncAwaitHelperTests.swift diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8a10bd7..003c563 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,11 +14,11 @@ on: jobs: test: name: Run tests - runs-on: macos-12 + runs-on: macos-13 steps: - name: Checkout uses: actions/checkout@v2 - name: Change Xcode - run: sudo xcode-select -s /Applications/Xcode_13.4.1.app + run: sudo xcode-select -s /Applications/Xcode_15.0.1.app - name: Build and test - run: swift test --enable-code-coverage --disable-automatic-resolution \ No newline at end of file + run: swift test --enable-code-coverage --disable-automatic-resolution diff --git a/Apexy.podspec b/Apexy.podspec index 88266fd..1ec5d96 100644 --- a/Apexy.podspec +++ b/Apexy.podspec @@ -1,27 +1,27 @@ Pod::Spec.new do |s| s.name = "Apexy" - s.version = "1.7.3" + s.version = "1.7.4" s.summary = "HTTP transport library" s.homepage = "https://github.com/RedMadRobot/apexy-ios" s.license = { :type => "MIT"} s.author = { "Alexander Ignatiev" => "ai@redmadrobot.com" } s.source = { :git => "https://github.com/RedMadRobot/apexy-ios.git", :tag => "#{s.version}" } - s.ios.deployment_target = "11.0" - s.tvos.deployment_target = "11.0" - s.osx.deployment_target = "10.13" - s.watchos.deployment_target = "4.0" + s.ios.deployment_target = "13.0" + s.tvos.deployment_target = "13.0" + s.osx.deployment_target = "10.15" + s.watchos.deployment_target = "6.0" s.swift_version = "5.3" s.subspec 'Core' do |sp| - sp.source_files = "Sources/Apexy/*.swift" + sp.source_files = "Sources/Apexy/**/*.swift" end s.subspec 'Alamofire' do |sp| sp.source_files = "Sources/ApexyAlamofire/*.swift" sp.dependency "Apexy/Core" - sp.dependency "Alamofire", '~>5.0' + sp.dependency "Alamofire", '~>5.6' end s.subspec 'URLSession' do |sp| diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 7b13f54..e3271bd 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -791,7 +791,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 42LRQS6X44; INFOPLIST_FILE = "Example/Supporting Files/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -812,7 +812,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = 42LRQS6X44; INFOPLIST_FILE = "Example/Supporting Files/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Example/Example/Sources/Business Logic/Service/BookService.swift b/Example/Example/Sources/Business Logic/Service/BookService.swift index 660a5e6..d652088 100644 --- a/Example/Example/Sources/Business Logic/Service/BookService.swift +++ b/Example/Example/Sources/Business Logic/Service/BookService.swift @@ -12,29 +12,18 @@ import ExampleAPI typealias Book = ExampleAPI.Book protocol BookService { - - @discardableResult - func fetchBooks(completion: @escaping (Result<[Book], Error>) -> Void) -> Progress - - @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) func fetchBooks() async throws -> [Book] } final class BookServiceImpl: BookService { - let apiClient: Client + let apiClient: ConcurrencyClient - init(apiClient: Client) { + init(apiClient: ConcurrencyClient) { self.apiClient = apiClient } - func fetchBooks(completion: @escaping (Result<[Book], Error>) -> Void) -> Progress { - let endpoint = BookListEndpoint() - return apiClient.request(endpoint, completionHandler: completion) - } - - @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) func fetchBooks() async throws -> [Book] { let endpoint = BookListEndpoint() return try await apiClient.request(endpoint) diff --git a/Example/Example/Sources/Business Logic/Service/FileService.swift b/Example/Example/Sources/Business Logic/Service/FileService.swift index 686a90d..d72f60a 100644 --- a/Example/Example/Sources/Business Logic/Service/FileService.swift +++ b/Example/Example/Sources/Business Logic/Service/FileService.swift @@ -10,46 +10,24 @@ import Apexy import ExampleAPI protocol FileService { - - @discardableResult - func upload(file: URL, completion: @escaping (Result) -> Void) -> Progress - - @discardableResult - func upload(stream: InputStream, size: Int, completion: @escaping (Result) -> Void) -> Progress - - @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) func upload(file: URL) async throws - - @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) func upload(stream: InputStream, size: Int) async throws } final class FileServiceImpl: FileService { - let apiClient: Client + let apiClient: ConcurrencyClient - init(apiClient: Client) { + init(apiClient: ConcurrencyClient) { self.apiClient = apiClient } - - func upload(file: URL, completion: @escaping (Result) -> Void) -> Progress { - let endpoint = FileUploadEndpoint(fileURL: file) - return apiClient.upload(endpoint, completionHandler: completion) - } - - func upload(stream: InputStream, size: Int, completion: @escaping (Result) -> Void) -> Progress { - let endpoint = StreamUploadEndpoint(stream: stream, size: size) - return apiClient.upload(endpoint, completionHandler: completion) - } - - @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + func upload(file: URL) async throws { let endpoint = FileUploadEndpoint(fileURL: file) return try await apiClient.upload(endpoint) } - @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) func upload(stream: InputStream, size: Int) async throws { let endpoint = StreamUploadEndpoint(stream: stream, size: size) return try await apiClient.upload(endpoint) diff --git a/Example/Example/Sources/Business Logic/ServiceLayer.swift b/Example/Example/Sources/Business Logic/ServiceLayer.swift index 7631679..e224fca 100644 --- a/Example/Example/Sources/Business Logic/ServiceLayer.swift +++ b/Example/Example/Sources/Business Logic/ServiceLayer.swift @@ -15,14 +15,12 @@ final class ServiceLayer { static let shared = ServiceLayer() - private(set) lazy var apiClient: Client = { - return AlamofireClient( - baseURL: URL(string: "https://library.mock-object.redmadserver.com/api/v1/")!, - configuration: .ephemeral, - responseObserver: { [weak self] request, response, data, error in - self?.validateSession(responseError: error) - }) - }() + private(set) lazy var apiClient: ConcurrencyClient = AlamofireClient( + baseURL: URL(string: "https://library.mock-object.redmadserver.com/api/v1/")!, + configuration: .ephemeral, + responseObserver: { [weak self] request, response, data, error in + self?.validateSession(responseError: error) + }) private(set) lazy var bookService: BookService = BookServiceImpl(apiClient: apiClient) diff --git a/Example/Example/Sources/Presentation/ViewController.swift b/Example/Example/Sources/Presentation/ViewController.swift index d5a7152..2a08bac 100644 --- a/Example/Example/Sources/Presentation/ViewController.swift +++ b/Example/Example/Sources/Presentation/ViewController.swift @@ -30,8 +30,6 @@ class ViewController: UIViewController { @IBAction private func performRequest() { activityView.isHidden = false - guard #available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) else { performLegacyRequest(); return } - task = Task { do { let books = try await bookService.fetchBooks() @@ -43,25 +41,9 @@ class ViewController: UIViewController { } } - private func performLegacyRequest() { - progress = bookService.fetchBooks() { [weak self] result in - guard let self = self else { return } - self.activityView.isHidden = true - switch result { - case .success(let books): - self.show(books: books) - case .failure(let error): - self.show(error: error) - } - } - } - - @IBAction private func upload() { guard let file = Bundle.main.url(forResource: "Info", withExtension: "plist") else { return } activityView.isHidden = false - - guard #available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) else { legacyUpload(with: file); return } task = Task { do { @@ -73,27 +55,12 @@ class ViewController: UIViewController { activityView.isHidden = true } } - - private func legacyUpload(with file: URL) { - progress = fileService.upload(file: file) { [weak self] result in - guard let self = self else { return } - self.activityView.isHidden = true - switch result { - case .success: - self.showOKUpload() - case .failure(let error): - self.show(error: error) - } - } - } - + @IBAction private func uploadStream() { let streamer = Streamer() self.streamer = streamer activityView.isHidden = false - guard #available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) else { legacyUploadStream(with: streamer); return } - streamer.run() task = Task { @@ -106,30 +73,6 @@ class ViewController: UIViewController { } } - private func legacyUploadStream(with streamer: Streamer) { - progress = fileService.upload( - stream: streamer.boundStreams.input, - size: streamer.totalDataSize) { [weak self] result in - guard let self = self else { return } - self.activityView.isHidden = true - switch result { - case .success: - self.showOKUpload() - case .failure(let error): - self.show(error: error) - self.streamer = nil - } - } - streamer.run() - - observation = progress?.observe(\.fractionCompleted, options: [.new]) { [weak self] (progress, value) in - DispatchQueue.main.async { - let percent = (value.newValue ?? 0) * 100 - self?.resultLabel.text = "Progress: \(String(format: "%.0f", percent))%" - } - } - } - @IBAction private func cancel() { if #available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) { (task as? Task)?.cancel() diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 09d4e82..7491936 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,11 +1,11 @@ PODS: - - Alamofire (5.4.1) - - Apexy (1.7.2): - - Apexy/Alamofire (= 1.7.2) - - Apexy/Alamofire (1.7.2): - - Alamofire (~> 5.0) + - Alamofire (5.8.1) + - Apexy (1.7.4): + - Apexy/Alamofire (= 1.7.4) + - Apexy/Alamofire (1.7.4): + - Alamofire (~> 5.6) - Apexy/Core - - Apexy/Core (1.7.2) + - Apexy/Core (1.7.4) DEPENDENCIES: - Apexy (from `../`) @@ -19,8 +19,8 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - Alamofire: 2291f7d21ca607c491dd17642e5d40fdcda0e65c - Apexy: 984a2c615adf4b5af3432859d768a7a3dec51907 + Alamofire: 3ca42e259043ee0dc5c0cdd76c4bc568b8e42af7 + Apexy: a3218097135e746fd7c9215da167521f9275df23 PODFILE CHECKSUM: f86a90e7590ccb3aa7caeceaf315abe256650c66 diff --git a/Package.swift b/Package.swift index 6528028..4915964 100644 --- a/Package.swift +++ b/Package.swift @@ -6,10 +6,10 @@ import PackageDescription let package = Package( name: "Apexy", platforms: [ - .macOS(.v10_13), - .iOS(.v11), - .tvOS(.v11), - .watchOS(.v4) + .macOS(.v10_15), + .iOS(.v13), + .tvOS(.v13), + .watchOS(.v6) ], products: [ .library(name: "Apexy", targets: ["ApexyURLSession"]), diff --git a/README.md b/README.md index 804865a..85cd057 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,7 @@ Split the network layer into folders: ## Requirements -- iOS 11.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+ +- iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ - Xcode 12+ - Swift 5.3+ diff --git a/README.ru.md b/README.ru.md index e87256b..8a21958 100644 --- a/README.ru.md +++ b/README.ru.md @@ -285,7 +285,7 @@ public struct FileUploadEndpoint: UploadEndpoint { ## Требования -- iOS 11.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+ +- iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ - Xcode 12+ - Swift 5.3+ diff --git a/Sources/ApexyURLSession/Helpers/AsyncAwaitHelper.swift b/Sources/ApexyURLSession/Helpers/AsyncAwaitHelper.swift deleted file mode 100644 index 71211ba..0000000 --- a/Sources/ApexyURLSession/Helpers/AsyncAwaitHelper.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) -enum AsyncAwaitHelper { - - enum AsyncError: Error, Equatable { - case cancelledBeforeStart - } - - public typealias ContentContinuation = CheckedContinuation - - public static func adaptToAsync(dataTaskClosure: (ContentContinuation) -> Progress) async throws -> T { - let progressWrapper = ProgressWrapper() - return try await withTaskCancellationHandler(handler: { - progressWrapper.cancel() - }, operation: { - try Task.checkCancellation() - return try await withCheckedThrowingContinuation { (continuation: ContentContinuation) in - let progress = dataTaskClosure(continuation) - progressWrapper.progress = progress - } - }) - } -} diff --git a/Sources/ApexyURLSession/Helpers/ProgressWrapper.swift b/Sources/ApexyURLSession/Helpers/ProgressWrapper.swift deleted file mode 100644 index bcf8b12..0000000 --- a/Sources/ApexyURLSession/Helpers/ProgressWrapper.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Foundation - -@propertyWrapper struct Locked { - - var wrappedValue: T { - get { - lock.lock() - defer { lock.unlock() } - return _value - } - set { - lock.lock() - defer { lock.unlock() } - _value = newValue - } - } - - private var _value: T - private let lock = NSLock() - - init(wrappedValue: T) { - self._value = wrappedValue - } -} - -@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) -final class ProgressWrapper { - - @Locked - var progress: Progress? - - init(_progress: Progress? = nil) { - self.progress = progress - } - - func cancel() { - progress?.cancel() - } -} diff --git a/Sources/ApexyURLSession/Helpers/URLSession+Concurrency.swift b/Sources/ApexyURLSession/Helpers/URLSession+Concurrency.swift deleted file mode 100644 index fc4ee8a..0000000 --- a/Sources/ApexyURLSession/Helpers/URLSession+Concurrency.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// URLSession+Concurrency.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 { - - /// Send request - /// - Parameters: - /// - request: A URL request object that provides the URL, cache policy, request type, and so on. The body stream and body data in this request object are ignored. - /// - delegate: Delegate to get events about request (NOT WORKING💀) - /// - Returns: Tuple with Data and URLResponse - public func data( - for request: URLRequest, - delegate: URLSessionTaskDelegate? = nil) async throws -> (Data, URLResponse) { - return try await AsyncAwaitHelper.adaptToAsync(dataTaskClosure: { continuation in - let task = 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)) - } - task.resume() - return task.progress - }) - } - - /// Send request - /// - Parameters: - /// - request: A URL request object that provides the URL, cache policy, request type, and so on. The body stream and body data in this request object are ignored. - /// - fromFile: The URL of the file to upload. - /// - delegate: Delegate to get events about request (NOT WORKING💀) - /// - Returns: Tuple with Data and URLResponse - public func upload( - for request: URLRequest, - fromFile fileURL: URL, - delegate: URLSessionTaskDelegate? = nil) async throws -> (Data, URLResponse) { - return try await AsyncAwaitHelper.adaptToAsync(dataTaskClosure: { continuation in - let task = 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)) - } - task.resume() - return task.progress - }) - } - - /// Send request - /// - Parameters: - /// - request: A URL request object that provides the URL, cache policy, request type, and so on. The body stream and body data in this request object are ignored. - /// - bodyData: The body data for the request. - /// - delegate: Delegate to get events about request (NOT WORKING💀) - /// - Returns: Tuple with Data and URLResponse - public func upload( - for request: URLRequest, - from bodyData: Data, - delegate: URLSessionTaskDelegate? = nil) async throws -> (Data, URLResponse) { - return try await AsyncAwaitHelper.adaptToAsync(dataTaskClosure: { continuation in - let task = 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)) - } - task.resume() - return task.progress - }) - } -} diff --git a/Sources/ApexyURLSession/URLSessionClient.swift b/Sources/ApexyURLSession/URLSessionClient.swift index 9490a34..71516ee 100644 --- a/Sources/ApexyURLSession/URLSessionClient.swift +++ b/Sources/ApexyURLSession/URLSessionClient.swift @@ -18,17 +18,20 @@ open class URLSessionClient: Client, CombineClient { /// - Parameters: /// - baseURL: Base `URL`. /// - configuration: The configuration used to construct the managed session. + /// - delegate: The delegate of URLSession. /// - completionQueue: The serial operation queue used to dispatch all completion handlers. `.main` by default. /// - responseObserver: The closure to be called after each response. public convenience init( baseURL: URL, configuration: URLSessionConfiguration = .default, + delegate: URLSessionDelegate? = nil, completionQueue: DispatchQueue = .main, responseObserver: ResponseObserver? = nil) { self.init( requestAdapter: BaseRequestAdapter(baseURL: baseURL), configuration: configuration, + delegate: delegate, completionQueue: completionQueue, responseObserver: responseObserver) } @@ -38,16 +41,18 @@ open class URLSessionClient: Client, CombineClient { /// - Parameters: /// - requestAdapter: RequestAdapter used to adapt a `URLRequest`. /// - configuration: The configuration used to construct the managed session. + /// - delegate: The delegate of URLSession. /// - completionQueue: The serial operation queue used to dispatch all completion handlers. `.main` by default. /// - responseObserver: The closure to be called after each response. public init( requestAdapter: RequestAdapter, configuration: URLSessionConfiguration = .default, + delegate: URLSessionDelegate? = nil, completionQueue: DispatchQueue = .main, responseObserver: ResponseObserver? = nil) { self.requestAdapter = requestAdapter - self.session = URLSession(configuration: configuration) + self.session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil) self.completionQueue = completionQueue self.responseObserver = responseObserver } diff --git a/Tests/ApexyURLSessionTests/AsyncAwaitHelperTests.swift b/Tests/ApexyURLSessionTests/AsyncAwaitHelperTests.swift deleted file mode 100644 index 0682178..0000000 --- a/Tests/ApexyURLSessionTests/AsyncAwaitHelperTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// AsyncAwitHelperTests.swift -// -// -// Created by Aleksei Tiurnin on 31.10.2022. -// - -import XCTest -@testable import ApexyURLSession - -final class AsyncAwaitHelperTests: XCTestCase { - - func testExample() async throws { - let task = Task(priority: .background) { - try await AsyncAwaitHelper.adaptToAsync(dataTaskClosure: { continuation in - continuation.resume(returning: "123") - return Progress() - }) - } - let value = try await task.value - XCTAssertEqual(value, "123") - } - - func testCancelExample() async throws { - let task = Task(priority: .background) { - try await AsyncAwaitHelper.adaptToAsync(dataTaskClosure: { continuation in - continuation.resume(returning: "123") - return Progress() - }) - } - task.cancel() - do { - _ = try await task.value - XCTFail("Task has been cancelled") - } catch { - XCTAssert(error is CancellationError) - } - } - -}