From e9de8a835f3eb5012fd1db8d2d4b5c1e0b43dbc9 Mon Sep 17 00:00:00 2001 From: Michael Amundsen Date: Mon, 21 Oct 2024 14:35:53 -0400 Subject: [PATCH 1/7] Conforming to sendable Signed-off-by: Michael Amundsen --- Example/AppDelegate.swift | 2 +- Networking.xcodeproj/project.pbxproj | 12 ++++++++---- Sources/Networking/HTTPMethod.swift | 2 +- Sources/Networking/NetworkController.swift | 6 +++--- Sources/Networking/NetworkError.swift | 2 +- Sources/Networking/NetworkRequest.swift | 2 +- Sources/Networking/NetworkRequestPerformer.swift | 4 ++-- .../Networking/NetworkRequestStateController.swift | 14 +++++++------- Sources/Networking/NetworkResponse.swift | 2 +- Sources/Networking/NetworkSession.swift | 2 +- Sources/Networking/NetworkSessionDataTask.swift | 2 +- Sources/Networking/RequestBehavior.swift | 2 +- 12 files changed, 28 insertions(+), 24 deletions(-) diff --git a/Example/AppDelegate.swift b/Example/AppDelegate.swift index af245ae..b964f17 100644 --- a/Example/AppDelegate.swift +++ b/Example/AppDelegate.swift @@ -8,7 +8,7 @@ import UIKit import Combine -@UIApplicationMain +@main class AppDelegate: UIResponder, UIApplicationDelegate { let controller = NetworkController() diff --git a/Networking.xcodeproj/project.pbxproj b/Networking.xcodeproj/project.pbxproj index 809aeee..8adbd4a 100644 --- a/Networking.xcodeproj/project.pbxproj +++ b/Networking.xcodeproj/project.pbxproj @@ -401,6 +401,8 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -454,6 +456,8 @@ SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; VALIDATE_PRODUCT = YES; }; name = Release; @@ -474,7 +478,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = net.lickability.Networking; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -495,7 +499,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = net.lickability.Networking; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -516,7 +520,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = net.lickability.NetworkingTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Networking.app/Networking"; }; @@ -538,7 +542,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = net.lickability.NetworkingTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Networking.app/Networking"; }; diff --git a/Sources/Networking/HTTPMethod.swift b/Sources/Networking/HTTPMethod.swift index 87d87fb..ab4296d 100644 --- a/Sources/Networking/HTTPMethod.swift +++ b/Sources/Networking/HTTPMethod.swift @@ -9,7 +9,7 @@ import Foundation /// Encapsulates HTTP methods for requests. -public enum HTTPMethod: String { +public enum HTTPMethod: String, Sendable { /// HTTP `GET`. case get = "GET" diff --git a/Sources/Networking/NetworkController.swift b/Sources/Networking/NetworkController.swift index 448d0cf..ac62ecc 100644 --- a/Sources/Networking/NetworkController.swift +++ b/Sources/Networking/NetworkController.swift @@ -10,7 +10,7 @@ import Foundation import Combine /// A default concrete implementation of the `NetworkRequestPerformer`. -public final class NetworkController { +public final class NetworkController: Sendable { private let networkSession: NetworkSession private let defaultRequestBehaviors: [RequestBehavior] @@ -33,7 +33,7 @@ public final class NetworkController { return urlRequest } - private func makeDataTask(forURLRequest urlRequest: URLRequest, behaviors: [RequestBehavior] = [], successHTTPStatusCodes: HTTPStatusCodes, completion: ((Result) -> Void)?) -> NetworkSessionDataTask { + private func makeDataTask(forURLRequest urlRequest: URLRequest, behaviors: [RequestBehavior] = [], successHTTPStatusCodes: HTTPStatusCodes, completion: (@Sendable (Result) -> Void)?) -> NetworkSessionDataTask { return networkSession.makeDataTask(with: urlRequest) { data, response, error in let result: Result @@ -61,7 +61,7 @@ extension NetworkController: NetworkRequestPerformer { // MARK: - NetworkRequestPerformer - @discardableResult public func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior] = [], completion: ((Result) -> Void)? = nil) -> NetworkSessionDataTask { + @discardableResult public func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior] = [], completion: (@Sendable (Result) -> Void)? = nil) -> NetworkSessionDataTask { let behaviors = defaultRequestBehaviors + requestBehaviors let urlRequest = makeFinalizedRequest(fromOriginalRequest: request.urlRequest, behaviors: behaviors) diff --git a/Sources/Networking/NetworkError.swift b/Sources/Networking/NetworkError.swift index 18864e0..5f9fe2d 100644 --- a/Sources/Networking/NetworkError.swift +++ b/Sources/Networking/NetworkError.swift @@ -9,7 +9,7 @@ import Foundation /// Possible errors encountered during networking. -public enum NetworkError: LocalizedError { +public enum NetworkError: LocalizedError, Sendable { // MARK: - NetworkError diff --git a/Sources/Networking/NetworkRequest.swift b/Sources/Networking/NetworkRequest.swift index aec0244..cb51586 100644 --- a/Sources/Networking/NetworkRequest.swift +++ b/Sources/Networking/NetworkRequest.swift @@ -9,7 +9,7 @@ import Foundation /// A protocol that defines the parameters that make up a request. -public protocol NetworkRequest: Equatable { +public protocol NetworkRequest: Equatable, Sendable { /// The generated `URLRequest` to use for making network requests. Defaults to a url request built using the receiver’s properties. var urlRequest: URLRequest { get } diff --git a/Sources/Networking/NetworkRequestPerformer.swift b/Sources/Networking/NetworkRequestPerformer.swift index 96cc2d8..a4a8937 100644 --- a/Sources/Networking/NetworkRequestPerformer.swift +++ b/Sources/Networking/NetworkRequestPerformer.swift @@ -10,7 +10,7 @@ import Foundation import Combine /// A protocol that defines functions needed to perform requests. -public protocol NetworkRequestPerformer { +public protocol NetworkRequestPerformer: Sendable { /// Performs the given request with the given behaviors. /// @@ -19,7 +19,7 @@ public protocol NetworkRequestPerformer { /// - requestBehaviors: The behaviors to apply to the given request. /// - completion: A completion closure that is called when the request has been completed. /// - Returns: The `NetworkSessionDataTask` used to send the request. The implementation must call `resume()` on the task before returning. - @discardableResult func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior], completion: ((Result) -> Void)?) -> NetworkSessionDataTask + @discardableResult func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior], completion: (@Sendable (Result) -> Void)?) -> NetworkSessionDataTask /// Returns a publisher that can be subscribed to, that performs the given request with the given behaviors. /// - Parameters: diff --git a/Sources/Networking/NetworkRequestStateController.swift b/Sources/Networking/NetworkRequestStateController.swift index 9a61fad..fe08392 100644 --- a/Sources/Networking/NetworkRequestStateController.swift +++ b/Sources/Networking/NetworkRequestStateController.swift @@ -7,10 +7,10 @@ // import Foundation -import Combine +@preconcurrency import Combine /// A class responsible for representing the state and value of a network request being made. -public final class NetworkRequestStateController { +public final class NetworkRequestStateController: Sendable { /// The state of a network request's lifecycle. public enum NetworkRequestState { @@ -76,18 +76,18 @@ public final class NetworkRequestStateController { } /// A `Publisher` that can be subscribed to in order to receive updates about the status of a request. - public private(set) lazy var publisher: AnyPublisher = { - return requestStatePublisher.prepend(.notInProgress).eraseToAnyPublisher() - }() + public let publisher: AnyPublisher private let requestPerformer: NetworkRequestPerformer - private let requestStatePublisher = PassthroughSubject() - private var cancellables = Set() + private let requestStatePublisher: PassthroughSubject + nonisolated(unsafe) private var cancellables = Set() /// Initializes the `NetworkRequestStateController` with the specified parameters. /// - Parameter requestPerformer: The `NetworkRequestPerformer` used to make requests. public init(requestPerformer: NetworkRequestPerformer) { + self.requestStatePublisher = PassthroughSubject() self.requestPerformer = requestPerformer + self.publisher = requestStatePublisher.prepend(.notInProgress).eraseToAnyPublisher() } /// Sends a request with the specified parameters. diff --git a/Sources/Networking/NetworkResponse.swift b/Sources/Networking/NetworkResponse.swift index caa1fb9..cedeab2 100644 --- a/Sources/Networking/NetworkResponse.swift +++ b/Sources/Networking/NetworkResponse.swift @@ -9,7 +9,7 @@ import Foundation /// A defined structure for a successful network response. -public struct NetworkResponse { +public struct NetworkResponse: Sendable { /// The data contained in the response. public let data: Data? diff --git a/Sources/Networking/NetworkSession.swift b/Sources/Networking/NetworkSession.swift index 897dcbc..594b83d 100644 --- a/Sources/Networking/NetworkSession.swift +++ b/Sources/Networking/NetworkSession.swift @@ -10,7 +10,7 @@ import Foundation import Combine /// Describes an object that coordinates a group of related, network data transfer tasks. This protocol has a similar API to `URLSession` for the purpose of mocking. -public protocol NetworkSession { +public protocol NetworkSession: Sendable { /// Creates a task that retrieves the contents of a URL based on the specified URL request object, and calls a handler upon completion. /// - Parameters: diff --git a/Sources/Networking/NetworkSessionDataTask.swift b/Sources/Networking/NetworkSessionDataTask.swift index 1bb0d32..86af11d 100644 --- a/Sources/Networking/NetworkSessionDataTask.swift +++ b/Sources/Networking/NetworkSessionDataTask.swift @@ -9,7 +9,7 @@ import Foundation /// Describes a network session task that can be performed. This protocol has a similar API to `URLSessionDataTask` for the purpose of mocking. -public protocol NetworkSessionDataTask { +public protocol NetworkSessionDataTask: Sendable { /// Progress object which represents the task progress. It can be used for task progress tracking. /// diff --git a/Sources/Networking/RequestBehavior.swift b/Sources/Networking/RequestBehavior.swift index 747eb8a..7d97421 100644 --- a/Sources/Networking/RequestBehavior.swift +++ b/Sources/Networking/RequestBehavior.swift @@ -9,7 +9,7 @@ import Foundation /// A protocol that can be used to implement behavior for requests being made. -public protocol RequestBehavior { +public protocol RequestBehavior: Sendable { /// A function that is called before a request is sent. You may modify the request at this time. /// From 50f8355b3e701878be4eef3cb4f990a8f05b704e Mon Sep 17 00:00:00 2001 From: Michael Amundsen Date: Mon, 21 Oct 2024 14:37:07 -0400 Subject: [PATCH 2/7] Adding sendable Signed-off-by: Michael Amundsen --- Sources/Networking/NetworkRequest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Networking/NetworkRequest.swift b/Sources/Networking/NetworkRequest.swift index cb51586..cd2cf51 100644 --- a/Sources/Networking/NetworkRequest.swift +++ b/Sources/Networking/NetworkRequest.swift @@ -37,7 +37,7 @@ public protocol NetworkRequest: Equatable, Sendable { } /// Represents a collection of possible HTTP status codes. -public enum HTTPStatusCodes: Equatable { +public enum HTTPStatusCodes: Equatable, Sendable { /// All status codes. case all From d85a077d61e4d0a7c6bcfae84f2c22f2045f9434 Mon Sep 17 00:00:00 2001 From: Michael Amundsen Date: Thu, 24 Oct 2024 11:18:28 -0400 Subject: [PATCH 3/7] Adjusting handling of sendable and threads Signed-off-by: Michael Amundsen --- Networking.xcodeproj/project.pbxproj | 8 ++++---- Sources/Networking/NetworkController.swift | 5 +++-- .../NetworkRequestPerformer+JSON.swift | 2 +- .../Networking/NetworkRequestPerformer.swift | 5 +++-- .../NetworkRequestStateController.swift | 17 ++++++++++++----- Tests/MockNetworkSession.swift | 13 ++++++++----- 6 files changed, 31 insertions(+), 19 deletions(-) diff --git a/Networking.xcodeproj/project.pbxproj b/Networking.xcodeproj/project.pbxproj index 8adbd4a..44c65e3 100644 --- a/Networking.xcodeproj/project.pbxproj +++ b/Networking.xcodeproj/project.pbxproj @@ -471,7 +471,7 @@ DEVELOPMENT_TEAM = JL4AKR8DVC; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Example/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -492,7 +492,7 @@ DEVELOPMENT_TEAM = JL4AKR8DVC; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Example/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -512,7 +512,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = JL4AKR8DVC; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.5; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -534,7 +534,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = JL4AKR8DVC; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.5; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Sources/Networking/NetworkController.swift b/Sources/Networking/NetworkController.swift index ac62ecc..e1017d0 100644 --- a/Sources/Networking/NetworkController.swift +++ b/Sources/Networking/NetworkController.swift @@ -72,12 +72,13 @@ extension NetworkController: NetworkRequestPerformer { return dataTask } - @available(iOS 13.0, *) - @discardableResult public func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior] = []) -> AnyPublisher { + @MainActor + @discardableResult public func send(_ request: any NetworkRequest, scheduler: some Scheduler = DispatchQueue.main, requestBehaviors: [RequestBehavior] = []) -> AnyPublisher { let behaviors = defaultRequestBehaviors + requestBehaviors let urlRequest = makeFinalizedRequest(fromOriginalRequest: request.urlRequest, behaviors: behaviors) return networkSession.dataTaskPublisher(for: urlRequest) + .receive(on: scheduler) .mapError { NetworkError.underlyingNetworkingError($0) } .tryMap { data, response in if let statusCode = (response as? HTTPURLResponse)?.statusCode, !request.successHTTPStatusCodes.contains(statusCode: statusCode) { diff --git a/Sources/Networking/NetworkRequestPerformer+JSON.swift b/Sources/Networking/NetworkRequestPerformer+JSON.swift index 10082d6..05cfe30 100644 --- a/Sources/Networking/NetworkRequestPerformer+JSON.swift +++ b/Sources/Networking/NetworkRequestPerformer+JSON.swift @@ -18,7 +18,7 @@ extension NetworkRequestPerformer { /// - decoder: The JSON decoder to use when decoding the data. /// - completion: A completion closure that is called when the request has been completed. /// - Returns: The `NetworkSessionDataTask` used to send the request. The implementation must call `resume()` on the task before returning. - @discardableResult public func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior] = [], decoder: JSONDecoder = JSONDecoder(), completion: ((Result) -> Void)? = nil) -> NetworkSessionDataTask { + @discardableResult public func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior] = [], decoder: JSONDecoder = JSONDecoder(), completion: (@Sendable (Result) -> Void)? = nil) -> NetworkSessionDataTask { send(request, requestBehaviors: requestBehaviors) { result in switch result { case let .success(response): diff --git a/Sources/Networking/NetworkRequestPerformer.swift b/Sources/Networking/NetworkRequestPerformer.swift index a4a8937..0f2134d 100644 --- a/Sources/Networking/NetworkRequestPerformer.swift +++ b/Sources/Networking/NetworkRequestPerformer.swift @@ -24,10 +24,11 @@ public protocol NetworkRequestPerformer: Sendable { /// Returns a publisher that can be subscribed to, that performs the given request with the given behaviors. /// - Parameters: /// - request: The request to perform. + /// - scheduler: The scheduler to receive the call on. The scheduler passed in must match the `@MainActor` requirement to avoid data races. /// - requestBehaviors: The behaviors to apply to the given request. /// - Returns: Returns a publisher that can be subscribed to, that performs the given request with the given behaviors. - @available(iOS 13.0, *) - @discardableResult func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior]) -> AnyPublisher + @MainActor + @discardableResult func send(_ request: any NetworkRequest, scheduler: some Scheduler, requestBehaviors: [RequestBehavior]) -> AnyPublisher /// Performs the given request with the given behaviors returning a `NetworkResponse` with async/await, or throwing an error if unsuccessful. /// diff --git a/Sources/Networking/NetworkRequestStateController.swift b/Sources/Networking/NetworkRequestStateController.swift index fe08392..25b5770 100644 --- a/Sources/Networking/NetworkRequestStateController.swift +++ b/Sources/Networking/NetworkRequestStateController.swift @@ -80,6 +80,7 @@ public final class NetworkRequestStateController: Sendable { private let requestPerformer: NetworkRequestPerformer private let requestStatePublisher: PassthroughSubject + private let cancellablesQueue = DispatchQueue(label: "net.lickability.Networking.NetworkRequestStateController.cancellable.queue") nonisolated(unsafe) private var cancellables = Set() /// Initializes the `NetworkRequestStateController` with the specified parameters. @@ -93,26 +94,32 @@ public final class NetworkRequestStateController: Sendable { /// Sends a request with the specified parameters. /// - Parameters: /// - request: The request to send. - /// - scheduler: The scheduler to receive the call on. The default value is `DispatchQueue.main`. + /// - scheduler: The scheduler to receive the call on. The scheduler passed in must match the `@MainActor` requirement to avoid data races. The default value is `DispatchQueue.main`. /// - requestBehaviors: Additional behaviors to append to the request. /// - retryCount: The number of times the action can be retried. + @MainActor public func send(request: any NetworkRequest, scheduler: some Scheduler = DispatchQueue.main, requestBehaviors: [RequestBehavior] = [], retryCount: Int = 2) { requestStatePublisher.send(.inProgress) - requestPerformer.send(request, requestBehaviors: requestBehaviors) + let cancellable = requestPerformer.send(request, scheduler: scheduler, requestBehaviors: requestBehaviors) .retry(retryCount) .mapAsResult() .receive(on: scheduler) .sink(receiveValue: { [requestStatePublisher] result in requestStatePublisher.send(.completed(result)) }) - .store(in: &cancellables) + + _ = cancellablesQueue.sync { + cancellables.insert(cancellable) + } } /// Resets the state of the `requestStatePublisher` and cancels any in flight requests that may be ongoing. Cancellation is not guaranteed, and requests that are near completion may end up finishing, despite being cancelled. public func resetState() { - cancellables.forEach { $0.cancel() } - cancellables.removeAll() + cancellablesQueue.sync { + cancellables.forEach { $0.cancel() } + cancellables.removeAll() + } requestStatePublisher.send(.notInProgress) } diff --git a/Tests/MockNetworkSession.swift b/Tests/MockNetworkSession.swift index bc4109f..3a2effb 100644 --- a/Tests/MockNetworkSession.swift +++ b/Tests/MockNetworkSession.swift @@ -10,7 +10,7 @@ import Foundation import Networking /// A mocked version of `NetworkSession` to be used in tests. Allows specification of success or failure cases. -class MockNetworkSession: NetworkSession { +final class MockNetworkSession: NetworkSession { private let result: Result /// Creates a new `MockNetworkSession`. @@ -41,10 +41,13 @@ class MockNetworkSession: NetworkSession { } } -private class MockNetworkSessionDataTask: NetworkSessionDataTask { - private let resumeClosure: () -> Void - - init(closure: @escaping () -> Void) { +private final class MockNetworkSessionDataTask: NetworkSessionDataTask { + + let progress = Progress() + + private let resumeClosure: @Sendable () -> Void + + init(closure: @escaping @Sendable () -> Void) { self.resumeClosure = closure } From d8502e29d49388aaec6c1fbb798605b1376597a1 Mon Sep 17 00:00:00 2001 From: Michael Amundsen Date: Thu, 24 Oct 2024 15:44:04 -0400 Subject: [PATCH 4/7] Removing default implementation due to unexpected behavior Signed-off-by: Michael Amundsen --- Sources/Networking/RequestBehavior.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Sources/Networking/RequestBehavior.swift b/Sources/Networking/RequestBehavior.swift index 7d97421..1a12e22 100644 --- a/Sources/Networking/RequestBehavior.swift +++ b/Sources/Networking/RequestBehavior.swift @@ -22,11 +22,6 @@ public protocol RequestBehavior: Sendable { func requestDidFinish(result: Result) } -public extension RequestBehavior { - func requestWillSend(request: inout URLRequest) { } - func requestDidFinish(result: Result) { } -} - extension Array: RequestBehavior where Element == RequestBehavior { public func requestWillSend(request: inout URLRequest) { forEach { $0.requestWillSend(request: &request) } From 3d00fe16b8297ee45cda5e01e9b489af216041fc Mon Sep 17 00:00:00 2001 From: Michael Amundsen Date: Thu, 24 Oct 2024 15:44:10 -0400 Subject: [PATCH 5/7] Adjusting test Signed-off-by: Michael Amundsen --- Tests/NetworkControllerTests.swift | 38 ++++++++++++++++-------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/Tests/NetworkControllerTests.swift b/Tests/NetworkControllerTests.swift index 9ffa09b..5e49918 100644 --- a/Tests/NetworkControllerTests.swift +++ b/Tests/NetworkControllerTests.swift @@ -61,37 +61,39 @@ class NetworkControllerTests: XCTestCase { func testAsyncAwaitBehaviors() async throws { let networkController = NetworkController(networkSession: MockNetworkSession(result: .failure(NetworkError.noResponse))) - - var requestWillSendWasCalled = false - var requestDidFinishWasCalled = false - - let behavior = TestBehavior { - requestWillSendWasCalled = true - XCTAssertFalse(requestDidFinishWasCalled, "We should’ve reached this point before `requestDidFinishWasCalled` became true.") - } didFinishClosure: { - requestDidFinishWasCalled = true + let expectation = expectation(description: "testAsyncAwaitBehaviors") + + let behavior = TestBehavior { willSend in + XCTAssertTrue(willSend) + } didFinishClosure: { didSend in + XCTAssertTrue(didSend) + expectation.fulfill() } - + do { let _: [Photo] = try await networkController.send(PhotoRequest.photosList, requestBehaviors: [behavior]) XCTFail("Should’ve caught an error before reaching here.") + } catch { + if let networkError = error as? NetworkError { + XCTAssertEqual(networkError.localizedDescription, NetworkError.noResponse.localizedDescription) + } else { + XCTFail("Expected to be able to cast error to NetworkError.") + } } - catch { - XCTAssertTrue(requestWillSendWasCalled) - XCTAssertTrue(requestDidFinishWasCalled) - } + + await fulfillment(of: [expectation], timeout: 0.5) } } private struct TestBehavior: RequestBehavior { - let willSendClosure: () -> Void - let didFinishClosure: () -> Void + let willSendClosure: @Sendable (Bool) -> Void + let didFinishClosure: @Sendable (Bool) -> Void func requestWillSend(request: inout URLRequest) { - willSendClosure() + willSendClosure(true) } func requestDidFinish(result: Result) { - didFinishClosure() + didFinishClosure(true) } } From e15ae644464181eb5fdd641c90db033054d0965a Mon Sep 17 00:00:00 2001 From: Michael Amundsen Date: Thu, 24 Oct 2024 15:47:19 -0400 Subject: [PATCH 6/7] Updating project file Signed-off-by: Michael Amundsen --- Networking.xcodeproj/project.pbxproj | 19 +++++++++++-------- .../xcschemes/Networking.xcscheme | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Networking.xcodeproj/project.pbxproj b/Networking.xcodeproj/project.pbxproj index 44c65e3..2261f08 100644 --- a/Networking.xcodeproj/project.pbxproj +++ b/Networking.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -234,8 +234,9 @@ F2B5BC1B248836C500B6A52A /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1150; - LastUpgradeCheck = 1150; + LastUpgradeCheck = 1600; ORGANIZATIONNAME = Lickability; TargetAttributes = { F2B5BC22248836C500B6A52A = { @@ -370,6 +371,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -380,6 +382,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -432,6 +435,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -442,6 +446,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -471,7 +476,7 @@ DEVELOPMENT_TEAM = JL4AKR8DVC; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Example/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -492,7 +497,7 @@ DEVELOPMENT_TEAM = JL4AKR8DVC; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Example/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -507,12 +512,11 @@ F2B5BC46248836C600B6A52A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = JL4AKR8DVC; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -529,12 +533,11 @@ F2B5BC47248836C600B6A52A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = JL4AKR8DVC; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Networking.xcodeproj/xcshareddata/xcschemes/Networking.xcscheme b/Networking.xcodeproj/xcshareddata/xcschemes/Networking.xcscheme index 3a4da30..140c9de 100644 --- a/Networking.xcodeproj/xcshareddata/xcschemes/Networking.xcscheme +++ b/Networking.xcodeproj/xcshareddata/xcschemes/Networking.xcscheme @@ -1,6 +1,6 @@ Date: Thu, 24 Oct 2024 15:48:15 -0400 Subject: [PATCH 7/7] Setting back to 13.5 Signed-off-by: Michael Amundsen --- Networking.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Networking.xcodeproj/project.pbxproj b/Networking.xcodeproj/project.pbxproj index 2261f08..8f63db5 100644 --- a/Networking.xcodeproj/project.pbxproj +++ b/Networking.xcodeproj/project.pbxproj @@ -476,7 +476,7 @@ DEVELOPMENT_TEAM = JL4AKR8DVC; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Example/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -497,7 +497,7 @@ DEVELOPMENT_TEAM = JL4AKR8DVC; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = Example/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -516,7 +516,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = JL4AKR8DVC; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -537,7 +537,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = JL4AKR8DVC; INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.5; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks",