Skip to content

Commit 1754b89

Browse files
authored
chore: strict concurrency in Swift 5.10 (#20)
* chore: strict concurrency in Swift 5.10 * chore: set exact version as 1.14 * chore: attempt to force 5.10.0 * chore: don’t specify a swift version for latest xcode * chore: makes mutating state functions public * chore: update pre-commit * chore: add test for cancel --------- Co-authored-by: danthorpe <[email protected]>
1 parent 84a5f27 commit 1754b89

File tree

17 files changed

+92
-42
lines changed

17 files changed

+92
-42
lines changed

.github/workflows/main.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ jobs:
2828
},
2929
{
3030
"os": "macos-14",
31-
"swift": "5.10",
3231
"xcode": "15.4"
3332
}
3433
]

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ repos:
1111
- id: check-executables-have-shebangs
1212
- id: check-shebang-scripts-are-executable
1313
- repo: https://github.com/realm/SwiftLint
14-
rev: 0.54.0
14+
rev: 0.57.0
1515
hooks:
1616
- id: swiftlint
1717
entry: swiftlint --fix --strict
1818
- repo: https://github.com/danthorpe/swift-format
19-
rev: 756fbb36972bd4ddcb6b3923ad38c0f8efda2d89
19+
rev: feba87c2da2a64e8aeff4ae00c9e65962a7f2f75
2020
hooks:
2121
- id: swift-format

Package.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,31 @@ var package = Package(
1414
],
1515
dependencies: [
1616
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"),
17-
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", from: "1.15.0"),
17+
.package(url: "https://github.com/pointfreeco/swift-composable-architecture", exact: "1.14.0"),
1818
],
1919
targets: [
2020
.target(
2121
name: "ComposableLoadable",
2222
dependencies: [
2323
.composableArchitecture
24-
]
24+
],
25+
swiftSettings: .concurrency
2526
),
2627
.target(
2728
name: "CommonTestHelpers",
2829
dependencies: [
2930
"ComposableLoadable",
3031
.composableArchitecture,
31-
]
32+
],
33+
swiftSettings: .concurrency
3234
),
3335
.testTarget(
3436
name: "ComposableLoadableTests",
3537
dependencies: [
3638
"CommonTestHelpers",
3739
"ComposableLoadable",
38-
]
40+
],
41+
swiftSettings: .concurrency
3942
),
4043
]
4144
)
@@ -45,3 +48,17 @@ extension Target.Dependency {
4548
name: "ComposableArchitecture", package: "swift-composable-architecture"
4649
)
4750
}
51+
52+
extension [SwiftSetting] {
53+
#if compiler(>=6)
54+
static let concurrency: Self = [
55+
.enableUpcomingFeature("StrictConcurrency")
56+
.enableUpcomingFeature("InferSendableFromCaptures")
57+
]
58+
#else
59+
static let concurrency: Self = [
60+
.enableExperimentalFeature("StrictConcurrency"),
61+
.enableExperimentalFeature("InferSendableFromCaptures"),
62+
]
63+
#endif
64+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#if compiler(<6.0) || !hasFeature(InferSendableFromCaptures)
2+
#warning("Workaround for a bunch of Strict Concurrency related warnings. To be removed when Swift 6.0 is available.")
3+
extension KeyPath: @unchecked Sendable {}
4+
#endif

Sources/CommonTestHelpers/TestFeatureClient.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import ComposableArchitecture
22

33
struct TestFeatureClient: TestDependencyKey {
4-
static var testValue = TestFeatureClient(
4+
static let testValue = TestFeatureClient(
55
getValue: unimplemented("TestFeatureClient.getValue")
66
)
77
var getValue: @Sendable (String) async throws -> Int

Sources/ComposableLoadable/Loadable/LoadableClient.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ package protocol LoadableClient<Request, State, Value> {
1010
var load: @Sendable (Request, State) async throws -> Value { get }
1111
}
1212

13-
public struct EmptyLoadRequest: Equatable {
13+
public struct EmptyLoadRequest: Equatable, Sendable {
1414
public init() {}
1515
}
1616

Sources/ComposableLoadable/Loadable/LoadableState.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,15 @@ public struct LoadableState<Request, Value> {
199199
}
200200
}
201201

202-
mutating func becomeActive(_ request: Request) {
202+
mutating public func becomeActive(_ request: Request) {
203203
current = .active(request)
204204
}
205205

206-
mutating func cancel() {
206+
mutating public func cancel() {
207207
current = previous ?? .pending
208208
}
209209

210-
mutating func finish(
210+
mutating public func finish(
211211
_ request: Request,
212212
result: Result<Value, Error>
213213
) {

Sources/ComposableLoadable/Loadable/LoadingAction.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import ComposableArchitecture
22
import Foundation
33

44
@CasePathable
5-
public enum LoadingAction<Request, Value, Action> {
5+
public enum LoadingAction<Request: Sendable, Value: Sendable, Action> {
66
case cancel
77
case finished(Request, didRefresh: Bool, TaskResult<Value>)
88
case load(Request)
@@ -22,6 +22,8 @@ extension LoadingAction where Request == EmptyLoadRequest {
2222

2323
// MARK: - Conformances
2424

25+
extension LoadingAction: Sendable where Request: Sendable, Value: Sendable, Action: Sendable {}
26+
2527
extension LoadingAction: Equatable where Value: Equatable {
2628
// NOTE: Define conformance here, but implementation is below
2729
}

Sources/ComposableLoadable/Loadable/LoadingReducer.swift

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import Foundation
66
extension Reducer {
77

88
/// Integrate a Loadable child domain with a generic Request type
9-
public func loadable<ChildState, ChildAction, Child: Reducer, Request>(
9+
public func loadable<
10+
ChildState: Sendable,
11+
ChildAction,
12+
Child: Reducer,
13+
Request: Sendable
14+
>(
1015
_ toLoadableState: WritableKeyPath<State, LoadableState<Request, ChildState>>,
1116
action toLoadingAction: CaseKeyPath<Action, LoadingAction<Request, ChildState, ChildAction>>,
1217
@ReducerBuilder<ChildState, ChildAction> child: () -> Child,
@@ -25,7 +30,11 @@ extension Reducer {
2530
}
2631

2732
/// Integrate a Loadable child domain which does not require a Request type
28-
public func loadable<ChildState, ChildAction, Child: Reducer>(
33+
public func loadable<
34+
ChildState: Sendable,
35+
ChildAction,
36+
Child: Reducer
37+
>(
2938
_ toLoadableState: WritableKeyPath<State, LoadableState<EmptyLoadRequest, ChildState>>,
3039
action toLoadingAction: CaseKeyPath<
3140
Action, LoadingAction<EmptyLoadRequest, ChildState, ChildAction>
@@ -46,7 +55,10 @@ extension Reducer {
4655
}
4756

4857
/// Integrate some LoadableState which does not require a child domain
49-
public func loadable<ChildState, Request>(
58+
public func loadable<
59+
ChildState: Sendable,
60+
Request: Sendable
61+
>(
5062
_ toLoadableState: WritableKeyPath<State, LoadableState<Request, ChildState>>,
5163
action toLoadingAction: CaseKeyPath<Action, LoadingAction<Request, ChildState, NoLoadingAction>>,
5264
load: @escaping @Sendable (Request, State) async throws -> ChildState,
@@ -71,7 +83,7 @@ extension Reducer {
7183

7284
fileprivate func loadable<
7385
Child: Reducer,
74-
Request,
86+
Request: Sendable,
7587
Client: LoadableClient<Request, State, Child.State>
7688
>(
7789
fileID: StaticString,
@@ -80,7 +92,7 @@ extension Reducer {
8092
action toLoadingAction: CaseKeyPath<Action, LoadingAction<Request, Child.State, Child.Action>>,
8193
client: Client,
8294
@ReducerBuilder<Child.State, Child.Action> child: () -> Child
83-
) -> LoadingReducer<Self, Child, Request, Client> {
95+
) -> LoadingReducer<Self, Child, Request, Client> where Client.State: Sendable {
8496
LoadingReducer(
8597
parent: self,
8698
child: child(),
@@ -98,9 +110,9 @@ extension Reducer {
98110
private struct LoadingReducer<
99111
Parent: Reducer,
100112
Child: Reducer,
101-
Request,
113+
Request: Sendable,
102114
Client: LoadableClient<Request, Parent.State, Child.State>
103-
>: Reducer {
115+
>: Reducer where Child.State: Sendable {
104116

105117
typealias State = Parent.State
106118
typealias Action = Parent.Action
@@ -155,16 +167,16 @@ private struct LoadingReducer<
155167
struct CancelID: Hashable {}
156168

157169
func reduce(into state: inout State, action: Action) -> Effect<Action> {
158-
let parentState = state
170+
let parentState = UncheckedSendable(state)
159171
return CombineReducers {
160172
Scope(state: toLoadableState, action: toLoadingAction) {
161173
Reduce { loadableState, loadingAction in
162174

163175
func send(refresh: Bool, _ request: Request) -> Effect<ThisLoadingAction> {
164176
loadableState.becomeActive(request)
165177
return
166-
.run { send in
167-
let value = try await client.load(request, parentState)
178+
.run { [load = client.load] send in
179+
let value = try await load(request, parentState.value)
168180
await send(.finished(request, didRefresh: refresh, .success(value)))
169181
} catch: { error, send in
170182
await send(.finished(request, didRefresh: refresh, .failure(error)))

Sources/ComposableLoadable/Pagination/PaginationContext.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import Foundation
1414
/// let searchId: String
1515
/// }
1616
/// ```
17-
public protocol PaginationContext: Equatable {
17+
public protocol PaginationContext: Equatable, Sendable {
1818

1919
/// A convenience to determine equality between any two
2020
/// contexts.

0 commit comments

Comments
 (0)