Skip to content

Commit b596de5

Browse files
authored
Fix cancellation of ViewStore.suspend (#725)
* Fix cancellation of ViewStore.suspend * wip * Remove capture list
1 parent 40a6c2c commit b596de5

File tree

1 file changed

+27
-15
lines changed

1 file changed

+27
-15
lines changed

Sources/ComposableArchitecture/Beta/Concurrency.swift

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,10 @@ import SwiftUI
109109
extension ViewStore {
110110
/// Sends an action into the store and then suspends while a piece of state is `true`.
111111
///
112-
/// This method can be used to interact with async/await code, allowing you to suspend while work
113-
/// is being performed in an effect. One common example of this is using SwiftUI's `.refreshable`
114-
/// method, which shows a loading indicator on the screen while work is being performed.
112+
/// This method can be used to interact with async/await code, allowing you to suspend while
113+
/// work is being performed in an effect. One common example of this is using SwiftUI's
114+
/// `.refreshable` method, which shows a loading indicator on the screen while work is being
115+
/// performed.
115116
///
116117
/// For example, suppose we wanted to load some data from the network when a pull-to-refresh
117118
/// gesture is performed on a list. The domain and logic for this feature can be modeled like so:
@@ -173,17 +174,19 @@ import SwiftUI
173174
///
174175
/// Here we've used the ``send(_:while:)`` method to suspend while the `isLoading` state is
175176
/// `true`. Once that piece of state flips back to `false` the method will resume, signaling to
176-
/// `.refreshable` that the work has finished which will cause the loading indicator to disappear.
177+
/// `.refreshable` that the work has finished which will cause the loading indicator to
178+
/// disappear.
177179
///
178180
/// **Note:** ``ViewStore`` is not thread safe and you should only send actions to it from the
179-
/// main thread. If you are wanting to send actions on background threads due to the fact that the
180-
/// reducer is performing computationally expensive work, then a better way to handle this is to
181-
/// wrap that work in an ``Effect`` that is performed on a background thread so that the result
182-
/// can be fed back into the store.
181+
/// main thread. If you are wanting to send actions on background threads due to the fact that
182+
/// the reducer is performing computationally expensive work, then a better way to handle this
183+
/// is to wrap that work in an ``Effect`` that is performed on a background thread so that the
184+
/// result can be fed back into the store.
183185
///
184186
/// - Parameters:
185187
/// - action: An action.
186-
/// - predicate: A predicate on `State` that determines for how long this method should suspend.
188+
/// - predicate: A predicate on `State` that determines for how long this method should
189+
/// suspend.
187190
public func send(
188191
_ action: Action,
189192
while predicate: @escaping (State) -> Bool
@@ -199,7 +202,8 @@ import SwiftUI
199202
/// - Parameters:
200203
/// - action: An action.
201204
/// - animation: The animation to perform when the action is sent.
202-
/// - predicate: A predicate on `State` that determines for how long this method should suspend.
205+
/// - predicate: A predicate on `State` that determines for how long this method should
206+
/// suspend.
203207
public func send(
204208
_ action: Action,
205209
animation: Animation?,
@@ -211,12 +215,12 @@ import SwiftUI
211215

212216
/// Suspends while a predicate on state is `true`.
213217
///
214-
/// - Parameter predicate: A predicate on `State` that determines for how long this method should
215-
/// suspend.
218+
/// - Parameter predicate: A predicate on `State` that determines for how long this method
219+
/// should suspend.
216220
public func suspend(while predicate: @escaping (State) -> Bool) async {
217-
var cancellable: Cancellable?
221+
let cancellable = Box<AnyCancellable?>(wrappedValue: nil)
218222
try? await withTaskCancellationHandler(
219-
handler: { [cancellable] in cancellable?.cancel() },
223+
handler: { cancellable.wrappedValue?.cancel() },
220224
operation: {
221225
try Task.checkCancellation()
222226
try await withUnsafeThrowingContinuation {
@@ -225,7 +229,7 @@ import SwiftUI
225229
continuation.resume(throwing: CancellationError())
226230
return
227231
}
228-
cancellable = self.publisher
232+
cancellable.wrappedValue = self.publisher
229233
.filter { !predicate($0) }
230234
.prefix(1)
231235
.sink { _ in
@@ -237,4 +241,12 @@ import SwiftUI
237241
)
238242
}
239243
}
244+
245+
private class Box<Value> {
246+
var wrappedValue: Value
247+
248+
init(wrappedValue: Value) {
249+
self.wrappedValue = wrappedValue
250+
}
251+
}
240252
#endif

0 commit comments

Comments
 (0)