Skip to content

Commit

Permalink
Merge pull request #18 from Lickability/feature/swift-6
Browse files Browse the repository at this point in the history
Swift 6 and concurrency update
  • Loading branch information
Cordavi authored Oct 29, 2024
2 parents c7013a3 + b3e0b28 commit bd7d435
Show file tree
Hide file tree
Showing 16 changed files with 86 additions and 70 deletions.
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>) { }
}

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

0 comments on commit bd7d435

Please sign in to comment.