Skip to content

Commit b7c5a40

Browse files
authored
Deprecate type-based cancel IDs (#2091)
* Deprecate type-based cancel IDs Swift may aggressively compile types out of release mode, including types defined for cancellation. Because of this, folks should migrate to use value-based identifiers and avoid any potential bugs. * wip * wip
1 parent 0460763 commit b7c5a40

39 files changed

+281
-534
lines changed

ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Animations.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ struct Animations: ReducerProtocol {
4141
@Dependency(\.continuousClock) var clock
4242

4343
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
44-
enum CancelID {}
44+
enum CancelID { case rainbow }
4545

4646
switch action {
4747
case .alertDismissed:
@@ -59,7 +59,7 @@ struct Animations: ReducerProtocol {
5959
try await self.clock.sleep(for: .seconds(1))
6060
}
6161
}
62-
.cancellable(id: CancelID.self)
62+
.cancellable(id: CancelID.rainbow)
6363

6464
case .resetButtonTapped:
6565
state.alert = AlertState {
@@ -79,7 +79,7 @@ struct Animations: ReducerProtocol {
7979

8080
case .resetConfirmationButtonTapped:
8181
state = State()
82-
return .cancel(id: CancelID.self)
82+
return .cancel(id: CancelID.rainbow)
8383

8484
case let .setColor(color):
8585
state.circleColor = color

Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Basics.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ struct EffectsBasics: ReducerProtocol {
3737

3838
@Dependency(\.continuousClock) var clock
3939
@Dependency(\.factClient) var factClient
40-
private enum DelayID {}
40+
private enum CancelID { case delay }
4141

4242
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
4343
switch action {
@@ -51,7 +51,7 @@ struct EffectsBasics: ReducerProtocol {
5151
try await self.clock.sleep(for: .seconds(1))
5252
return .decrementDelayResponse
5353
}
54-
.cancellable(id: DelayID.self)
54+
.cancellable(id: CancelID.delay)
5555

5656
case .decrementDelayResponse:
5757
if state.count < 0 {
@@ -63,7 +63,7 @@ struct EffectsBasics: ReducerProtocol {
6363
state.count += 1
6464
state.numberFact = nil
6565
return state.count >= 0
66-
? .cancel(id: DelayID.self)
66+
? .cancel(id: CancelID.delay)
6767
: .none
6868

6969
case .numberFactButtonTapped:

Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Cancellation.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,19 @@ struct EffectsCancellation: ReducerProtocol {
2929
}
3030

3131
@Dependency(\.factClient) var factClient
32-
private enum NumberFactRequestID {}
32+
private enum CancelID { case factRequest }
3333

3434
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
3535
switch action {
3636
case .cancelButtonTapped:
3737
state.isFactRequestInFlight = false
38-
return .cancel(id: NumberFactRequestID.self)
38+
return .cancel(id: CancelID.factRequest)
3939

4040
case let .stepperChanged(value):
4141
state.count = value
4242
state.currentFact = nil
4343
state.isFactRequestInFlight = false
44-
return .cancel(id: NumberFactRequestID.self)
44+
return .cancel(id: CancelID.factRequest)
4545

4646
case .factButtonTapped:
4747
state.currentFact = nil
@@ -50,7 +50,7 @@ struct EffectsCancellation: ReducerProtocol {
5050
return .task { [count = state.count] in
5151
await .factResponse(TaskResult { try await self.factClient.fetch(count) })
5252
}
53-
.cancellable(id: NumberFactRequestID.self)
53+
.cancellable(id: CancelID.factRequest)
5454

5555
case let .factResponse(.success(response)):
5656
state.isFactRequestInFlight = false

Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Refreshable.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ struct Refreshable: ReducerProtocol {
2828
}
2929

3030
@Dependency(\.factClient) var factClient
31-
private enum FactRequestID {}
31+
private enum CancelID { case factRequest }
3232

3333
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
3434
switch action {
3535
case .cancelButtonTapped:
36-
return .cancel(id: FactRequestID.self)
36+
return .cancel(id: CancelID.factRequest)
3737

3838
case .decrementButtonTapped:
3939
state.count -= 1
@@ -57,7 +57,7 @@ struct Refreshable: ReducerProtocol {
5757
await .factResponse(TaskResult { try await self.factClient.fetch(count) })
5858
}
5959
.animation()
60-
.cancellable(id: FactRequestID.self)
60+
.cancellable(id: CancelID.factRequest)
6161
}
6262
}
6363
}

Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Timers.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ struct Timers: ReducerProtocol {
2424
}
2525

2626
@Dependency(\.continuousClock) var clock
27-
private enum TimerID {}
27+
private enum CancelID { case timer }
2828

2929
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
3030
switch action {
3131
case .onDisappear:
32-
return .cancel(id: TimerID.self)
32+
return .cancel(id: CancelID.timer)
3333

3434
case .timerTicked:
3535
state.secondsElapsed += 1
@@ -43,7 +43,7 @@ struct Timers: ReducerProtocol {
4343
await send(.timerTicked, animation: .interpolatingSpring(stiffness: 3000, damping: 40))
4444
}
4545
}
46-
.cancellable(id: TimerID.self, cancelInFlight: true)
46+
.cancellable(id: CancelID.timer, cancelInFlight: true)
4747
}
4848
}
4949
}

Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ struct WebSocket: ReducerProtocol {
3838

3939
@Dependency(\.continuousClock) var clock
4040
@Dependency(\.webSocket) var webSocket
41-
private enum WebSocketID {}
4241

4342
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
4443
switch action {
@@ -50,13 +49,13 @@ struct WebSocket: ReducerProtocol {
5049
switch state.connectivityState {
5150
case .connected, .connecting:
5251
state.connectivityState = .disconnected
53-
return .cancel(id: WebSocketID.self)
52+
return .cancel(id: WebSocketClient.ID())
5453

5554
case .disconnected:
5655
state.connectivityState = .connecting
5756
return .run { send in
5857
let actions = await self.webSocket
59-
.open(WebSocketID.self, URL(string: "wss://echo.websocket.events")!, [])
58+
.open(WebSocketClient.ID(), URL(string: "wss://echo.websocket.events")!, [])
6059
await withThrowingTaskGroup(of: Void.self) { group in
6160
for await action in actions {
6261
// NB: Can't call `await send` here outside of `group.addTask` due to task local
@@ -69,11 +68,11 @@ struct WebSocket: ReducerProtocol {
6968
group.addTask {
7069
while !Task.isCancelled {
7170
try await self.clock.sleep(for: .seconds(10))
72-
try? await self.webSocket.sendPing(WebSocketID.self)
71+
try? await self.webSocket.sendPing(WebSocketClient.ID())
7372
}
7473
}
7574
group.addTask {
76-
for await result in try await self.webSocket.receive(WebSocketID.self) {
75+
for await result in try await self.webSocket.receive(WebSocketClient.ID()) {
7776
await send(.receivedSocketMessage(result))
7877
}
7978
}
@@ -83,7 +82,7 @@ struct WebSocket: ReducerProtocol {
8382
}
8483
}
8584
}
86-
.cancellable(id: WebSocketID.self)
85+
.cancellable(id: WebSocketClient.ID())
8786
}
8887

8988
case let .messageToSendChanged(message):
@@ -103,12 +102,12 @@ struct WebSocket: ReducerProtocol {
103102
let messageToSend = state.messageToSend
104103
state.messageToSend = ""
105104
return .task {
106-
try await self.webSocket.send(WebSocketID.self, .string(messageToSend))
105+
try await self.webSocket.send(WebSocketClient.ID(), .string(messageToSend))
107106
return .sendResponse(didSucceed: true)
108107
} catch: { _ in
109108
.sendResponse(didSucceed: false)
110109
}
111-
.cancellable(id: WebSocketID.self)
110+
.cancellable(id: WebSocketClient.ID())
112111

113112
case .sendResponse(didSucceed: false):
114113
state.alert = AlertState {
@@ -121,7 +120,7 @@ struct WebSocket: ReducerProtocol {
121120

122121
case .webSocket(.didClose):
123122
state.connectivityState = .disconnected
124-
return .cancel(id: WebSocketID.self)
123+
return .cancel(id: WebSocketClient.ID())
125124

126125
case .webSocket(.didOpen):
127126
state.connectivityState = .connected
@@ -190,6 +189,19 @@ struct WebSocketView: View {
190189
// MARK: - WebSocketClient
191190

192191
struct WebSocketClient {
192+
struct ID: Hashable, @unchecked Sendable {
193+
let rawValue: AnyHashable
194+
195+
init<RawValue: Hashable & Sendable>(_ rawValue: RawValue) {
196+
self.rawValue = rawValue
197+
}
198+
199+
init() {
200+
struct RawValue: Hashable, Sendable {}
201+
self.rawValue = RawValue()
202+
}
203+
}
204+
193205
enum Action: Equatable {
194206
case didOpen(protocol: String?)
195207
case didClose(code: URLSessionWebSocketTask.CloseCode, reason: Data?)
@@ -210,10 +222,10 @@ struct WebSocketClient {
210222
}
211223
}
212224

213-
var open: @Sendable (Any.Type, URL, [String]) async -> AsyncStream<Action>
214-
var receive: @Sendable (Any.Type) async throws -> AsyncStream<TaskResult<Message>>
215-
var send: @Sendable (Any.Type, URLSessionWebSocketTask.Message) async throws -> Void
216-
var sendPing: @Sendable (Any.Type) async throws -> Void
225+
var open: @Sendable (ID, URL, [String]) async -> AsyncStream<Action>
226+
var receive: @Sendable (ID) async throws -> AsyncStream<TaskResult<Message>>
227+
var send: @Sendable (ID, URLSessionWebSocketTask.Message) async throws -> Void
228+
var sendPing: @Sendable (ID) async throws -> Void
217229
}
218230

219231
extension WebSocketClient: DependencyKey {
@@ -252,10 +264,9 @@ extension WebSocketClient: DependencyKey {
252264

253265
static let shared = WebSocketActor()
254266

255-
var dependencies: [ObjectIdentifier: Dependencies] = [:]
267+
var dependencies: [ID: Dependencies] = [:]
256268

257-
func open(id: Any.Type, url: URL, protocols: [String]) -> AsyncStream<Action> {
258-
let id = ObjectIdentifier(id)
269+
func open(id: ID, url: URL, protocols: [String]) -> AsyncStream<Action> {
259270
let delegate = Delegate()
260271
let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
261272
let socket = session.webSocketTask(with: url, protocols: protocols)
@@ -274,15 +285,14 @@ extension WebSocketClient: DependencyKey {
274285
}
275286

276287
func close(
277-
id: Any.Type, with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?
288+
id: ID, with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?
278289
) async throws {
279-
let id = ObjectIdentifier(id)
280290
defer { self.dependencies[id] = nil }
281291
try self.socket(id: id).cancel(with: closeCode, reason: reason)
282292
}
283293

284-
func receive(id: Any.Type) throws -> AsyncStream<TaskResult<Message>> {
285-
let socket = try self.socket(id: ObjectIdentifier(id))
294+
func receive(id: ID) throws -> AsyncStream<TaskResult<Message>> {
295+
let socket = try self.socket(id: id)
286296
return AsyncStream { continuation in
287297
let task = Task {
288298
while !Task.isCancelled {
@@ -294,12 +304,12 @@ extension WebSocketClient: DependencyKey {
294304
}
295305
}
296306

297-
func send(id: Any.Type, message: URLSessionWebSocketTask.Message) async throws {
298-
try await self.socket(id: ObjectIdentifier(id)).send(message)
307+
func send(id: ID, message: URLSessionWebSocketTask.Message) async throws {
308+
try await self.socket(id: id).send(message)
299309
}
300310

301-
func sendPing(id: Any.Type) async throws {
302-
let socket = try self.socket(id: ObjectIdentifier(id))
311+
func sendPing(id: ID) async throws {
312+
let socket = try self.socket(id: id)
303313
return try await withCheckedThrowingContinuation { continuation in
304314
socket.sendPing { error in
305315
if let error = error {
@@ -311,15 +321,15 @@ extension WebSocketClient: DependencyKey {
311321
}
312322
}
313323

314-
private func socket(id: ObjectIdentifier) throws -> URLSessionWebSocketTask {
324+
private func socket(id: ID) throws -> URLSessionWebSocketTask {
315325
guard let dependencies = self.dependencies[id]?.socket else {
316326
struct Closed: Error {}
317327
throw Closed()
318328
}
319329
return dependencies
320330
}
321331

322-
private func removeDependencies(id: ObjectIdentifier) {
332+
private func removeDependencies(id: ID) {
323333
self.dependencies[id] = nil
324334
}
325335
}

Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Lists-LoadThenNavigate.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ struct LoadThenNavigateList: ReducerProtocol {
3535
}
3636

3737
@Dependency(\.continuousClock) var clock
38-
private enum CancelID {}
38+
private enum CancelID { case load }
3939

4040
var body: some ReducerProtocol<State, Action> {
4141
Reduce { state, action in
@@ -44,7 +44,7 @@ struct LoadThenNavigateList: ReducerProtocol {
4444
return .none
4545

4646
case .onDisappear:
47-
return .cancel(id: CancelID.self)
47+
return .cancel(id: CancelID.load)
4848

4949
case let .setNavigation(selection: .some(navigatedId)):
5050
for row in state.rows {
@@ -54,14 +54,14 @@ struct LoadThenNavigateList: ReducerProtocol {
5454
try await self.clock.sleep(for: .seconds(1))
5555
return .setNavigationSelectionDelayCompleted(navigatedId)
5656
}
57-
.cancellable(id: CancelID.self, cancelInFlight: true)
57+
.cancellable(id: CancelID.load, cancelInFlight: true)
5858

5959
case .setNavigation(selection: .none):
6060
if let selection = state.selection {
6161
state.rows[id: selection.id]?.count = selection.count
6262
}
6363
state.selection = nil
64-
return .cancel(id: CancelID.self)
64+
return .cancel(id: CancelID.load)
6565

6666
case let .setNavigationSelectionDelayCompleted(id):
6767
state.rows[id: id]?.isActivityIndicatorVisible = false

Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Lists-NavigateAndLoad.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ struct NavigateAndLoadList: ReducerProtocol {
3232
}
3333

3434
@Dependency(\.continuousClock) var clock
35-
private enum CancelID {}
35+
private enum CancelID { case load }
3636

3737
var body: some ReducerProtocol<State, Action> {
3838
Reduce { state, action in
@@ -46,14 +46,14 @@ struct NavigateAndLoadList: ReducerProtocol {
4646
try await self.clock.sleep(for: .seconds(1))
4747
return .setNavigationSelectionDelayCompleted
4848
}
49-
.cancellable(id: CancelID.self, cancelInFlight: true)
49+
.cancellable(id: CancelID.load, cancelInFlight: true)
5050

5151
case .setNavigation(selection: .none):
5252
if let selection = state.selection, let count = selection.value?.count {
5353
state.rows[id: selection.id]?.count = count
5454
}
5555
state.selection = nil
56-
return .cancel(id: CancelID.self)
56+
return .cancel(id: CancelID.load)
5757

5858
case .setNavigationSelectionDelayCompleted:
5959
guard let id = state.selection?.id else { return .none }

0 commit comments

Comments
 (0)