Skip to content

Commit bd7d435

Browse files
authored
Merge pull request #18 from Lickability/feature/swift-6
Swift 6 and concurrency update
2 parents c7013a3 + b3e0b28 commit bd7d435

16 files changed

+86
-70
lines changed

Example/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import UIKit
1010
import Combine
11-
@UIApplicationMain
11+
@main
1212
class AppDelegate: UIResponder, UIApplicationDelegate {
1313

1414
let controller = NetworkController()

Networking.xcodeproj/project.pbxproj

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
archiveVersion = 1;
44
classes = {
55
};
6-
objectVersion = 50;
6+
objectVersion = 54;
77
objects = {
88

99
/* Begin PBXBuildFile section */
@@ -234,8 +234,9 @@
234234
F2B5BC1B248836C500B6A52A /* Project object */ = {
235235
isa = PBXProject;
236236
attributes = {
237+
BuildIndependentTargetsInParallel = YES;
237238
LastSwiftUpdateCheck = 1150;
238-
LastUpgradeCheck = 1150;
239+
LastUpgradeCheck = 1600;
239240
ORGANIZATIONNAME = Lickability;
240241
TargetAttributes = {
241242
F2B5BC22248836C500B6A52A = {
@@ -370,6 +371,7 @@
370371
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
371372
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
372373
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
374+
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
373375
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
374376
CLANG_WARN_STRICT_PROTOTYPES = YES;
375377
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -380,6 +382,7 @@
380382
DEBUG_INFORMATION_FORMAT = dwarf;
381383
ENABLE_STRICT_OBJC_MSGSEND = YES;
382384
ENABLE_TESTABILITY = YES;
385+
ENABLE_USER_SCRIPT_SANDBOXING = YES;
383386
GCC_C_LANGUAGE_STANDARD = gnu11;
384387
GCC_DYNAMIC_NO_PIC = NO;
385388
GCC_NO_COMMON_BLOCKS = YES;
@@ -401,6 +404,8 @@
401404
SDKROOT = iphoneos;
402405
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
403406
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
407+
SWIFT_STRICT_CONCURRENCY = complete;
408+
SWIFT_VERSION = 6.0;
404409
};
405410
name = Debug;
406411
};
@@ -430,6 +435,7 @@
430435
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
431436
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
432437
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
438+
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
433439
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
434440
CLANG_WARN_STRICT_PROTOTYPES = YES;
435441
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -440,6 +446,7 @@
440446
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
441447
ENABLE_NS_ASSERTIONS = NO;
442448
ENABLE_STRICT_OBJC_MSGSEND = YES;
449+
ENABLE_USER_SCRIPT_SANDBOXING = YES;
443450
GCC_C_LANGUAGE_STANDARD = gnu11;
444451
GCC_NO_COMMON_BLOCKS = YES;
445452
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -454,6 +461,8 @@
454461
SDKROOT = iphoneos;
455462
SWIFT_COMPILATION_MODE = wholemodule;
456463
SWIFT_OPTIMIZATION_LEVEL = "-O";
464+
SWIFT_STRICT_CONCURRENCY = complete;
465+
SWIFT_VERSION = 6.0;
457466
VALIDATE_PRODUCT = YES;
458467
};
459468
name = Release;
@@ -467,14 +476,14 @@
467476
DEVELOPMENT_TEAM = JL4AKR8DVC;
468477
ENABLE_PREVIEWS = YES;
469478
INFOPLIST_FILE = Example/Info.plist;
470-
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
479+
IPHONEOS_DEPLOYMENT_TARGET = 13.5;
471480
LD_RUNPATH_SEARCH_PATHS = (
472481
"$(inherited)",
473482
"@executable_path/Frameworks",
474483
);
475484
PRODUCT_BUNDLE_IDENTIFIER = net.lickability.Networking;
476485
PRODUCT_NAME = "$(TARGET_NAME)";
477-
SWIFT_VERSION = 5.0;
486+
SWIFT_VERSION = 6.0;
478487
TARGETED_DEVICE_FAMILY = "1,2";
479488
};
480489
name = Debug;
@@ -488,22 +497,21 @@
488497
DEVELOPMENT_TEAM = JL4AKR8DVC;
489498
ENABLE_PREVIEWS = YES;
490499
INFOPLIST_FILE = Example/Info.plist;
491-
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
500+
IPHONEOS_DEPLOYMENT_TARGET = 13.5;
492501
LD_RUNPATH_SEARCH_PATHS = (
493502
"$(inherited)",
494503
"@executable_path/Frameworks",
495504
);
496505
PRODUCT_BUNDLE_IDENTIFIER = net.lickability.Networking;
497506
PRODUCT_NAME = "$(TARGET_NAME)";
498-
SWIFT_VERSION = 5.0;
507+
SWIFT_VERSION = 6.0;
499508
TARGETED_DEVICE_FAMILY = "1,2";
500509
};
501510
name = Release;
502511
};
503512
F2B5BC46248836C600B6A52A /* Debug */ = {
504513
isa = XCBuildConfiguration;
505514
buildSettings = {
506-
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
507515
BUNDLE_LOADER = "$(TEST_HOST)";
508516
CODE_SIGN_STYLE = Automatic;
509517
DEVELOPMENT_TEAM = JL4AKR8DVC;
@@ -516,7 +524,7 @@
516524
);
517525
PRODUCT_BUNDLE_IDENTIFIER = net.lickability.NetworkingTests;
518526
PRODUCT_NAME = "$(TARGET_NAME)";
519-
SWIFT_VERSION = 5.0;
527+
SWIFT_VERSION = 6.0;
520528
TARGETED_DEVICE_FAMILY = "1,2";
521529
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Networking.app/Networking";
522530
};
@@ -525,7 +533,6 @@
525533
F2B5BC47248836C600B6A52A /* Release */ = {
526534
isa = XCBuildConfiguration;
527535
buildSettings = {
528-
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
529536
BUNDLE_LOADER = "$(TEST_HOST)";
530537
CODE_SIGN_STYLE = Automatic;
531538
DEVELOPMENT_TEAM = JL4AKR8DVC;
@@ -538,7 +545,7 @@
538545
);
539546
PRODUCT_BUNDLE_IDENTIFIER = net.lickability.NetworkingTests;
540547
PRODUCT_NAME = "$(TARGET_NAME)";
541-
SWIFT_VERSION = 5.0;
548+
SWIFT_VERSION = 6.0;
542549
TARGETED_DEVICE_FAMILY = "1,2";
543550
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Networking.app/Networking";
544551
};

Networking.xcodeproj/xcshareddata/xcschemes/Networking.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1150"
3+
LastUpgradeVersion = "1600"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

Sources/Networking/HTTPMethod.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import Foundation
1010

1111
/// Encapsulates HTTP methods for requests.
12-
public enum HTTPMethod: String {
12+
public enum HTTPMethod: String, Sendable {
1313

1414
/// HTTP `GET`.
1515
case get = "GET"

Sources/Networking/NetworkController.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010
import Combine
1111

1212
/// A default concrete implementation of the `NetworkRequestPerformer`.
13-
public final class NetworkController {
13+
public final class NetworkController: Sendable {
1414

1515
private let networkSession: NetworkSession
1616
private let defaultRequestBehaviors: [RequestBehavior]
@@ -33,7 +33,7 @@ public final class NetworkController {
3333
return urlRequest
3434
}
3535

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

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

6262
// MARK: - NetworkRequestPerformer
6363

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

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

75-
@available(iOS 13.0, *)
76-
@discardableResult public func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior] = []) -> AnyPublisher<NetworkResponse, NetworkError> {
75+
@MainActor
76+
@discardableResult public func send(_ request: any NetworkRequest, scheduler: some Scheduler = DispatchQueue.main, requestBehaviors: [RequestBehavior] = []) -> AnyPublisher<NetworkResponse, NetworkError> {
7777
let behaviors = defaultRequestBehaviors + requestBehaviors
7878
let urlRequest = makeFinalizedRequest(fromOriginalRequest: request.urlRequest, behaviors: behaviors)
7979

8080
return networkSession.dataTaskPublisher(for: urlRequest)
81+
.receive(on: scheduler)
8182
.mapError { NetworkError.underlyingNetworkingError($0) }
8283
.tryMap { data, response in
8384
if let statusCode = (response as? HTTPURLResponse)?.statusCode, !request.successHTTPStatusCodes.contains(statusCode: statusCode) {

Sources/Networking/NetworkError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import Foundation
1010

1111
/// Possible errors encountered during networking.
12-
public enum NetworkError: LocalizedError {
12+
public enum NetworkError: LocalizedError, Sendable {
1313

1414
// MARK: - NetworkError
1515

Sources/Networking/NetworkRequest.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import Foundation
1010

1111
/// A protocol that defines the parameters that make up a request.
12-
public protocol NetworkRequest: Equatable {
12+
public protocol NetworkRequest: Equatable, Sendable {
1313

1414
/// The generated `URLRequest` to use for making network requests. Defaults to a url request built using the receiver’s properties.
1515
var urlRequest: URLRequest { get }
@@ -37,7 +37,7 @@ public protocol NetworkRequest: Equatable {
3737
}
3838

3939
/// Represents a collection of possible HTTP status codes.
40-
public enum HTTPStatusCodes: Equatable {
40+
public enum HTTPStatusCodes: Equatable, Sendable {
4141

4242
/// All status codes.
4343
case all

Sources/Networking/NetworkRequestPerformer+JSON.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ extension NetworkRequestPerformer {
1818
/// - decoder: The JSON decoder to use when decoding the data.
1919
/// - completion: A completion closure that is called when the request has been completed.
2020
/// - Returns: The `NetworkSessionDataTask` used to send the request. The implementation must call `resume()` on the task before returning.
21-
@discardableResult public func send<ResponseType: Decodable>(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior] = [], decoder: JSONDecoder = JSONDecoder(), completion: ((Result<ResponseType, NetworkError>) -> Void)? = nil) -> NetworkSessionDataTask {
21+
@discardableResult public func send<ResponseType: Decodable>(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior] = [], decoder: JSONDecoder = JSONDecoder(), completion: (@Sendable (Result<ResponseType, NetworkError>) -> Void)? = nil) -> NetworkSessionDataTask {
2222
send(request, requestBehaviors: requestBehaviors) { result in
2323
switch result {
2424
case let .success(response):

Sources/Networking/NetworkRequestPerformer.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010
import Combine
1111

1212
/// A protocol that defines functions needed to perform requests.
13-
public protocol NetworkRequestPerformer {
13+
public protocol NetworkRequestPerformer: Sendable {
1414

1515
/// Performs the given request with the given behaviors.
1616
///
@@ -19,15 +19,16 @@ public protocol NetworkRequestPerformer {
1919
/// - requestBehaviors: The behaviors to apply to the given request.
2020
/// - completion: A completion closure that is called when the request has been completed.
2121
/// - Returns: The `NetworkSessionDataTask` used to send the request. The implementation must call `resume()` on the task before returning.
22-
@discardableResult func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior], completion: ((Result<NetworkResponse, NetworkError>) -> Void)?) -> NetworkSessionDataTask
22+
@discardableResult func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior], completion: (@Sendable (Result<NetworkResponse, NetworkError>) -> Void)?) -> NetworkSessionDataTask
2323

2424
/// Returns a publisher that can be subscribed to, that performs the given request with the given behaviors.
2525
/// - Parameters:
2626
/// - request: The request to perform.
27+
/// - scheduler: The scheduler to receive the call on. The scheduler passed in must match the `@MainActor` requirement to avoid data races.
2728
/// - requestBehaviors: The behaviors to apply to the given request.
2829
/// - Returns: Returns a publisher that can be subscribed to, that performs the given request with the given behaviors.
29-
@available(iOS 13.0, *)
30-
@discardableResult func send(_ request: any NetworkRequest, requestBehaviors: [RequestBehavior]) -> AnyPublisher<NetworkResponse, NetworkError>
30+
@MainActor
31+
@discardableResult func send(_ request: any NetworkRequest, scheduler: some Scheduler, requestBehaviors: [RequestBehavior]) -> AnyPublisher<NetworkResponse, NetworkError>
3132

3233
/// Performs the given request with the given behaviors returning a `NetworkResponse` with async/await, or throwing an error if unsuccessful.
3334
///

Sources/Networking/NetworkRequestStateController.swift

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
//
88

99
import Foundation
10-
import Combine
10+
@preconcurrency import Combine
1111

1212
/// A class responsible for representing the state and value of a network request being made.
13-
public final class NetworkRequestStateController {
13+
public final class NetworkRequestStateController: Sendable {
1414

1515
/// The state of a network request's lifecycle.
1616
public enum NetworkRequestState {
@@ -76,43 +76,50 @@ public final class NetworkRequestStateController {
7676
}
7777

7878
/// A `Publisher` that can be subscribed to in order to receive updates about the status of a request.
79-
public private(set) lazy var publisher: AnyPublisher<NetworkRequestState, Never> = {
80-
return requestStatePublisher.prepend(.notInProgress).eraseToAnyPublisher()
81-
}()
79+
public let publisher: AnyPublisher<NetworkRequestState, Never>
8280

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

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

9394
/// Sends a request with the specified parameters.
9495
/// - Parameters:
9596
/// - request: The request to send.
96-
/// - scheduler: The scheduler to receive the call on. The default value is `DispatchQueue.main`.
97+
/// - 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`.
9798
/// - requestBehaviors: Additional behaviors to append to the request.
9899
/// - retryCount: The number of times the action can be retried.
100+
@MainActor
99101
public func send(request: any NetworkRequest, scheduler: some Scheduler = DispatchQueue.main, requestBehaviors: [RequestBehavior] = [], retryCount: Int = 2) {
100102
requestStatePublisher.send(.inProgress)
101103

102-
requestPerformer.send(request, requestBehaviors: requestBehaviors)
104+
let cancellable = requestPerformer.send(request, scheduler: scheduler, requestBehaviors: requestBehaviors)
103105
.retry(retryCount)
104106
.mapAsResult()
105107
.receive(on: scheduler)
106108
.sink(receiveValue: { [requestStatePublisher] result in
107109
requestStatePublisher.send(.completed(result))
108110
})
109-
.store(in: &cancellables)
111+
112+
_ = cancellablesQueue.sync {
113+
cancellables.insert(cancellable)
114+
}
110115
}
111116

112117
/// 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.
113118
public func resetState() {
114-
cancellables.forEach { $0.cancel() }
115-
cancellables.removeAll()
119+
cancellablesQueue.sync {
120+
cancellables.forEach { $0.cancel() }
121+
cancellables.removeAll()
122+
}
116123

117124
requestStatePublisher.send(.notInProgress)
118125
}

0 commit comments

Comments
 (0)