Skip to content

Commit 05ec04c

Browse files
committed
Enable strict concurrency
1 parent 6fa3cb1 commit 05ec04c

File tree

8 files changed

+89
-46
lines changed

8 files changed

+89
-46
lines changed

Package.swift

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
// swift-tools-version:5.9
1+
// swift-tools-version:6.0
2+
23
import PackageDescription
34

45
let package = Package(
@@ -20,12 +21,14 @@ let package = Package(
2021
.target(
2122
name: "SwiftReachability",
2223
path: "Sources",
23-
resources: [.copy("Resources/PrivacyInfo.xcprivacy")]
24+
resources: [.copy("Resources/PrivacyInfo.xcprivacy")],
25+
swiftSettings: [.enableExperimentalFeature("StrictConcurrency")]
2426
),
2527
.testTarget(
2628
name: "SwiftReachabilityTests",
2729
dependencies: ["SwiftReachability"],
2830
path: "Tests"
2931
)
30-
]
32+
],
33+
swiftLanguageModes: [.v6]
3134
)

[email protected]

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// swift-tools-version:5.10
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "swift-reachability",
7+
platforms: [
8+
.iOS(.v15),
9+
.macOS(.v12),
10+
.tvOS(.v15),
11+
.watchOS(.v8),
12+
.visionOS(.v1)
13+
],
14+
products: [
15+
.library(
16+
name: "SwiftReachability",
17+
targets: ["SwiftReachability"]
18+
)
19+
],
20+
targets: [
21+
.target(
22+
name: "SwiftReachability",
23+
path: "Sources",
24+
resources: [.copy("Resources/PrivacyInfo.xcprivacy")]
25+
),
26+
.testTarget(
27+
name: "SwiftReachabilityTests",
28+
dependencies: ["SwiftReachability"],
29+
path: "Tests"
30+
)
31+
],
32+
swiftLanguageVersions: [.v5]
33+
)

Sources/Extensions/AsyncStream.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ extension AsyncSequence {
66

77
extension AsyncStream {
88
public init<S: AsyncSequence>(_ sequence: S) where S.Element == Element {
9-
var iterator: S.AsyncIterator?
9+
nonisolated(unsafe) let sequence = sequence
10+
nonisolated(unsafe) var iterator: S.AsyncIterator?
1011
self.init {
1112
if iterator == nil { iterator = sequence.makeAsyncIterator() }
1213
return try? await iterator?.next()

Sources/PathMonitor/InterfaceType.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Network
22

3-
protocol InterfaceType {
3+
protocol InterfaceType: Sendable {
44
var type: NWInterface.InterfaceType { get }
55
}
66

Sources/PathMonitor/PathMonitorType.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ extension NWPathMonitor: PathMonitorType {
4747
#endif
4848
var path: PathType { currentPath }
4949

50-
func onPathUpdate(_ callback: @escaping (PathType) -> Void) {
50+
func onPathUpdate(_ callback: @escaping @Sendable (PathType) -> Void) {
5151
pathUpdateHandler = { callback($0) }
5252
}
5353
}

Sources/Reachability/Reachability.swift

+18-16
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
import Network
22
import Combine
33

4-
@MainActor
5-
public final class Reachability: Sendable, ObservableObject {
4+
public actor Reachability: Sendable, ObservableObject {
65
public static let shared = Reachability()
76

87
private let monitor: PathMonitorType
98

10-
@Published public private(set) var status: ConnectionStatus
11-
@Published public private(set) var isExpensive: Bool
12-
@Published public private(set) var isConstrained: Bool
9+
@Published public private(set) var status: ConnectionStatus = .disconnected(.notAvailable)
10+
@Published public private(set) var isExpensive: Bool = false
11+
@Published public private(set) var isConstrained: Bool = false
1312

1413
init(monitor: PathMonitorType = NWPathMonitor()) {
1514
self.monitor = monitor
16-
self.status = monitor.connectionStatus(for: monitor.path)
17-
self.isExpensive = monitor.path.isExpensive
18-
self.isConstrained = monitor.path.isConstrained
19-
observeNetworkPathChanges()
15+
Task { [weak self] in
16+
await self?.networkPathChanged(monitor.path)
17+
await self?.observeNetworkPathChanges()
18+
}
2019
}
2120

2221
deinit {
@@ -61,18 +60,21 @@ public final class Reachability: Sendable, ObservableObject {
6160
.values
6261
.eraseToStream()
6362
}
64-
}
6563

66-
extension Reachability {
64+
// MARK: - Private
65+
6766
private func observeNetworkPathChanges() {
6867
monitor.start(queue: DispatchQueue.global(qos: .utility))
6968
monitor.onPathUpdate { path in
70-
DispatchQueue.main.async { [weak self] in
71-
guard let self else { return }
72-
status = monitor.connectionStatus(for: path)
73-
isExpensive = path.isExpensive
74-
isConstrained = path.isConstrained
69+
Task { [weak self] in
70+
await self?.networkPathChanged(path)
7571
}
7672
}
7773
}
74+
75+
private func networkPathChanged(_ path: PathType) {
76+
status = monitor.connectionStatus(for: path)
77+
isExpensive = path.isExpensive
78+
isConstrained = path.isConstrained
79+
}
7880
}

Tests/Helpers/FuncCheck.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
final class FuncCheck<Argument: Sendable>: @unchecked Sendable {
2-
var argument: Argument?
1+
final class FuncCheck<Argument: Sendable>: Sendable {
2+
nonisolated(unsafe) var argument: Argument?
33
var wasCalled: Bool { argument != nil }
44

55
func call(_ argument: Argument) {

Tests/ReachabilityTests.swift

+26-22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import XCTest
2-
import Combine
2+
@preconcurrency import Combine
33
#if os(iOS)
44
import CoreTelephony
55
#endif
@@ -15,8 +15,7 @@ final class ReachabilityTests: XCTestCase {
1515
bag.removeAll()
1616
}
1717

18-
@MainActor
19-
func test_isExpensive_shouldReturnCorrectValue() {
18+
func test_isExpensive_shouldReturnCorrectValue() async {
2019
let path = MockPath(status: .satisfied, isExpensive: true)
2120
#if os(iOS)
2221
let mockTelephonyInfo = MockTelephonyInfo()
@@ -25,12 +24,12 @@ final class ReachabilityTests: XCTestCase {
2524
let monitor = MockPathMonitor(path: path)
2625
#endif
2726
let networkReachability = Reachability(monitor: monitor)
28-
let isExpensive = networkReachability.isExpensive
27+
await Task.yield()
28+
let isExpensive = await networkReachability.isExpensive
2929
XCTAssert(isExpensive)
3030
}
3131

32-
@MainActor
33-
func test_isConstrained_shouldReturnCorrectValue() {
32+
func test_isConstrained_shouldReturnCorrectValue() async {
3433
let path = MockPath(status: .satisfied, isConstrained: true)
3534
#if os(iOS)
3635
let mockTelephonyInfo = MockTelephonyInfo()
@@ -39,12 +38,12 @@ final class ReachabilityTests: XCTestCase {
3938
let monitor = MockPathMonitor(path: path)
4039
#endif
4140
let networkReachability = Reachability(monitor: monitor)
42-
let isConstrained = networkReachability.isConstrained
41+
await Task.yield()
42+
let isConstrained = await networkReachability.isConstrained
4343
XCTAssert(isConstrained)
4444
}
4545

46-
@MainActor
47-
func test_status_shouldReturnConnectionStatus() {
46+
func test_status_shouldReturnConnectionStatus() async {
4847
let path = MockPath(status: .satisfied, availableInterfaceTypes: .wifi)
4948
#if os(iOS)
5049
let mockTelephonyInfo = MockTelephonyInfo()
@@ -53,11 +52,11 @@ final class ReachabilityTests: XCTestCase {
5352
let monitor = MockPathMonitor(path: path)
5453
#endif
5554
let networkReachability = Reachability(monitor: monitor)
56-
let connectionStatus = networkReachability.status
55+
await Task.yield()
56+
let connectionStatus = await networkReachability.status
5757
XCTAssertEqual(connectionStatus, .connected(.wifi))
5858
}
5959

60-
@MainActor
6160
func test_changes_whenConnectionStatusChanges_shouldNotify() async {
6261
let path = MockPath(status: .satisfied, availableInterfaceTypes: .wifi)
6362
#if os(iOS)
@@ -67,6 +66,7 @@ final class ReachabilityTests: XCTestCase {
6766
let monitor = MockPathMonitor(path: path)
6867
#endif
6968
let networkReachability = Reachability(monitor: monitor)
69+
await Task.yield()
7070
let expectation = XCTestExpectation(description: "connection status changed")
7171
Task.detached {
7272
for await status in await networkReachability.changes() {
@@ -90,7 +90,6 @@ final class ReachabilityTests: XCTestCase {
9090
await fulfillment(of: [expectation])
9191
}
9292

93-
@MainActor
9493
func test_changesPublisher_whenConnectionStatusChanges_shouldNotify() async {
9594
let path = MockPath(status: .satisfied, availableInterfaceTypes: .wifi)
9695
#if os(iOS)
@@ -100,8 +99,10 @@ final class ReachabilityTests: XCTestCase {
10099
let monitor = MockPathMonitor(path: path)
101100
#endif
102101
let networkReachability = Reachability(monitor: monitor)
102+
await Task.yield()
103103
let expectation = XCTestExpectation(description: "connection status changed")
104-
networkReachability.changesPublisher().sink { status in
104+
let publisher = await networkReachability.changesPublisher()
105+
publisher.sink { status in
105106
#if os(iOS)
106107
XCTAssertEqual(status, .connected(.cellular(.cellular4G)))
107108
#else
@@ -121,7 +122,6 @@ final class ReachabilityTests: XCTestCase {
121122
await fulfillment(of: [expectation])
122123
}
123124

124-
@MainActor
125125
func test_changes_whenConnectionStatusDidNotChange_shouldNotNotify() async {
126126
let path = MockPath(status: .satisfied, availableInterfaceTypes: .wifi)
127127
#if os(iOS)
@@ -131,6 +131,7 @@ final class ReachabilityTests: XCTestCase {
131131
let monitor = MockPathMonitor(path: path)
132132
#endif
133133
let networkReachability = Reachability(monitor: monitor)
134+
await Task.yield()
134135
let expectation = XCTestExpectation(description: "connection status changed")
135136
Task.detached {
136137
for await status in await networkReachability.changes() {
@@ -156,7 +157,6 @@ final class ReachabilityTests: XCTestCase {
156157
await fulfillment(of: [expectation])
157158
}
158159

159-
@MainActor
160160
func test_changesPublisher_whenConnectionStatusDidNotChange_shouldNotNotify() async {
161161
let path = MockPath(status: .satisfied, availableInterfaceTypes: .wifi)
162162
#if os(iOS)
@@ -166,8 +166,10 @@ final class ReachabilityTests: XCTestCase {
166166
let monitor = MockPathMonitor(path: path)
167167
#endif
168168
let networkReachability = Reachability(monitor: monitor)
169+
await Task.yield()
169170
let expectation = XCTestExpectation(description: "connection status changed")
170-
networkReachability.changesPublisher().sink { status in
171+
let publisher = await networkReachability.changesPublisher()
172+
publisher.sink { status in
171173
#if os(iOS)
172174
XCTAssertEqual(status, .connected(.cellular(.cellular4G)))
173175
#else
@@ -189,7 +191,6 @@ final class ReachabilityTests: XCTestCase {
189191
await fulfillment(of: [expectation])
190192
}
191193

192-
@MainActor
193194
func test_expensiveChanges_whenCostChanges_shouldNotify() async {
194195
let path = MockPath(status: .satisfied, isExpensive: true)
195196
#if os(iOS)
@@ -199,6 +200,7 @@ final class ReachabilityTests: XCTestCase {
199200
let monitor = MockPathMonitor(path: path)
200201
#endif
201202
let networkReachability = Reachability(monitor: monitor)
203+
await Task.yield()
202204
let expectation = XCTestExpectation(description: "cost changed")
203205
Task.detached {
204206
for await isExpensive in await networkReachability.expensiveChanges() {
@@ -214,7 +216,6 @@ final class ReachabilityTests: XCTestCase {
214216
await fulfillment(of: [expectation])
215217
}
216218

217-
@MainActor
218219
func test_expensiveChangesPublisher_whenCostChanges_shouldNotify() async {
219220
let path = MockPath(status: .satisfied, isExpensive: true)
220221
#if os(iOS)
@@ -224,8 +225,10 @@ final class ReachabilityTests: XCTestCase {
224225
let monitor = MockPathMonitor(path: path)
225226
#endif
226227
let networkReachability = Reachability(monitor: monitor)
228+
await Task.yield()
227229
let expectation = XCTestExpectation(description: "cost changed")
228-
networkReachability.expensiveChangesPublisher().sink { isExpensive in
230+
let publisher = await networkReachability.expensiveChangesPublisher()
231+
publisher.sink { isExpensive in
229232
XCTAssertFalse(isExpensive)
230233
expectation.fulfill()
231234
}.store(in: &bag)
@@ -237,7 +240,6 @@ final class ReachabilityTests: XCTestCase {
237240
await fulfillment(of: [expectation])
238241
}
239242

240-
@MainActor
241243
func test_constrainedChanges_whenRestrictionsChanges_shouldNotify() async {
242244
let path = MockPath(status: .satisfied, isConstrained: true)
243245
#if os(iOS)
@@ -247,6 +249,7 @@ final class ReachabilityTests: XCTestCase {
247249
let monitor = MockPathMonitor(path: path)
248250
#endif
249251
let networkReachability = Reachability(monitor: monitor)
252+
await Task.yield()
250253
let expectation = XCTestExpectation(description: "restriction changed")
251254
Task.detached {
252255
for await isConstrained in await networkReachability.constrainedChanges() {
@@ -262,7 +265,6 @@ final class ReachabilityTests: XCTestCase {
262265
await fulfillment(of: [expectation])
263266
}
264267

265-
@MainActor
266268
func test_constrainedChangesPublisher_whenRestrictionsChanges_shouldNotify() async {
267269
let path = MockPath(status: .satisfied, isConstrained: true)
268270
#if os(iOS)
@@ -272,8 +274,10 @@ final class ReachabilityTests: XCTestCase {
272274
let monitor = MockPathMonitor(path: path)
273275
#endif
274276
let networkReachability = Reachability(monitor: monitor)
277+
await Task.yield()
275278
let expectation = XCTestExpectation(description: "restriction changed")
276-
networkReachability.constrainedChangesPublisher().sink { isConstrained in
279+
let publisher = await networkReachability.constrainedChangesPublisher()
280+
publisher.sink { isConstrained in
277281
XCTAssertFalse(isConstrained)
278282
expectation.fulfill()
279283
}.store(in: &bag)

0 commit comments

Comments
 (0)