Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift 6 and concurrency update #18

Merged
merged 7 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Example/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import UIKit
import Combine
@UIApplicationMain
@main
class AppDelegate: UIResponder, UIApplicationDelegate {

let controller = NetworkController()
Expand Down
27 changes: 17 additions & 10 deletions Networking.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -234,8 +234,9 @@
F2B5BC1B248836C500B6A52A /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1150;
LastUpgradeCheck = 1150;
LastUpgradeCheck = 1600;
ORGANIZATIONNAME = Lickability;
TargetAttributes = {
F2B5BC22248836C500B6A52A = {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -401,6 +404,8 @@
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 6.0;
};
name = Debug;
};
Expand Down Expand Up @@ -430,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;
Expand All @@ -440,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;
Expand All @@ -454,6 +461,8 @@
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 6.0;
VALIDATE_PRODUCT = YES;
};
name = Release;
Expand All @@ -467,14 +476,14 @@
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",
);
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;
Expand All @@ -488,22 +497,21 @@
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",
);
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;
};
F2B5BC46248836C600B6A52A /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = JL4AKR8DVC;
Expand All @@ -516,7 +524,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";
};
Expand All @@ -525,7 +533,6 @@
F2B5BC47248836C600B6A52A /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = JL4AKR8DVC;
Expand All @@ -538,7 +545,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";
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1150"
LastUpgradeVersion = "1600"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
2 changes: 1 addition & 1 deletion Sources/Networking/HTTPMethod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 6 additions & 5 deletions Sources/Networking/NetworkController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -33,7 +33,7 @@ public final class NetworkController {
return urlRequest
}

private func makeDataTask(forURLRequest urlRequest: URLRequest, behaviors: [RequestBehavior] = [], successHTTPStatusCodes: HTTPStatusCodes, completion: ((Result<NetworkResponse, NetworkError>) -> Void)?) -> NetworkSessionDataTask {
private func makeDataTask(forURLRequest urlRequest: URLRequest, behaviors: [RequestBehavior] = [], successHTTPStatusCodes: HTTPStatusCodes, completion: (@Sendable (Result<NetworkResponse, NetworkError>) -> Void)?) -> NetworkSessionDataTask {

return networkSession.makeDataTask(with: urlRequest) { data, response, error in
let result: Result<NetworkResponse, NetworkError>
Expand Down Expand Up @@ -61,7 +61,7 @@ extension NetworkController: NetworkRequestPerformer {

// MARK: - NetworkRequestPerformer

@discardableResult public func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior] = [], completion: ((Result<NetworkResponse, NetworkError>) -> Void)? = nil) -> NetworkSessionDataTask {
@discardableResult public func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior] = [], completion: (@Sendable (Result<NetworkResponse, NetworkError>) -> Void)? = nil) -> NetworkSessionDataTask {
let behaviors = defaultRequestBehaviors + requestBehaviors

let urlRequest = makeFinalizedRequest(fromOriginalRequest: request.urlRequest, behaviors: behaviors)
Expand All @@ -72,12 +72,13 @@ extension NetworkController: NetworkRequestPerformer {
return dataTask
}

@available(iOS 13.0, *)
@discardableResult public func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior] = []) -> AnyPublisher<NetworkResponse, NetworkError> {
@MainActor
@discardableResult public func send(_ request: any NetworkRequest, scheduler: some Scheduler = DispatchQueue.main, requestBehaviors: [RequestBehavior] = []) -> AnyPublisher<NetworkResponse, NetworkError> {
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) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Networking/NetworkError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import Foundation

/// Possible errors encountered during networking.
public enum NetworkError: LocalizedError {
public enum NetworkError: LocalizedError, Sendable {

// MARK: - NetworkError

Expand Down
4 changes: 2 additions & 2 deletions Sources/Networking/NetworkRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -37,7 +37,7 @@ public protocol NetworkRequest: Equatable {
}

/// Represents a collection of possible HTTP status codes.
public enum HTTPStatusCodes: Equatable {
public enum HTTPStatusCodes: Equatable, Sendable {

/// All status codes.
case all
Expand Down
2 changes: 1 addition & 1 deletion Sources/Networking/NetworkRequestPerformer+JSON.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<ResponseType: Decodable>(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior] = [], decoder: JSONDecoder = JSONDecoder(), completion: ((Result<ResponseType, NetworkError>) -> Void)? = nil) -> NetworkSessionDataTask {
@discardableResult public func send<ResponseType: Decodable>(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior] = [], decoder: JSONDecoder = JSONDecoder(), completion: (@Sendable (Result<ResponseType, NetworkError>) -> Void)? = nil) -> NetworkSessionDataTask {
send(request, requestBehaviors: requestBehaviors) { result in
switch result {
case let .success(response):
Expand Down
9 changes: 5 additions & 4 deletions Sources/Networking/NetworkRequestPerformer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -19,15 +19,16 @@ 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<NetworkResponse, NetworkError>) -> Void)?) -> NetworkSessionDataTask
@discardableResult func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior], completion: (@Sendable (Result<NetworkResponse, NetworkError>) -> Void)?) -> NetworkSessionDataTask

/// 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<NetworkResponse, NetworkError>
@MainActor
@discardableResult func send(_ request: any NetworkRequest, scheduler: some Scheduler, requestBehaviors: [RequestBehavior]) -> AnyPublisher<NetworkResponse, NetworkError>

/// Performs the given request with the given behaviors returning a `NetworkResponse` with async/await, or throwing an error if unsuccessful.
///
Expand Down
31 changes: 19 additions & 12 deletions Sources/Networking/NetworkRequestStateController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -76,43 +76,50 @@ 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<NetworkRequestState, Never> = {
return requestStatePublisher.prepend(.notInProgress).eraseToAnyPublisher()
}()
public let publisher: AnyPublisher<NetworkRequestState, Never>

private let requestPerformer: NetworkRequestPerformer
private let requestStatePublisher = PassthroughSubject<NetworkRequestState, Never>()
private var cancellables = Set<AnyCancellable>()
private let requestStatePublisher: PassthroughSubject<NetworkRequestState, Never>
private let cancellablesQueue = DispatchQueue(label: "net.lickability.Networking.NetworkRequestStateController.cancellable.queue")
nonisolated(unsafe) private var cancellables = Set<AnyCancellable>()

/// Initializes the `NetworkRequestStateController` with the specified parameters.
/// - Parameter requestPerformer: The `NetworkRequestPerformer` used to make requests.
public init(requestPerformer: NetworkRequestPerformer) {
self.requestStatePublisher = PassthroughSubject<NetworkRequestState, Never>()
self.requestPerformer = requestPerformer
self.publisher = requestStatePublisher.prepend(.notInProgress).eraseToAnyPublisher()
}

/// 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)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Networking/NetworkResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
2 changes: 1 addition & 1 deletion Sources/Networking/NetworkSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Sources/Networking/NetworkSessionDataTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
7 changes: 1 addition & 6 deletions Sources/Networking/RequestBehavior.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -22,11 +22,6 @@ public protocol RequestBehavior {
func requestDidFinish(result: Result<NetworkResponse, NetworkError>)
}

public extension RequestBehavior {
func requestWillSend(request: inout URLRequest) { }
func requestDidFinish(result: Result<NetworkResponse, NetworkError>) { }
}

Comment on lines -25 to -29
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These seem to be the get called even when you conform the type which causes unexpected behavior. Removing them so you have to determine what you want.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure we mark the next version as a new major version then, as this is a breaking change.

extension Array: RequestBehavior where Element == RequestBehavior {
public func requestWillSend(request: inout URLRequest) {
forEach { $0.requestWillSend(request: &request) }
Expand Down
Loading