Skip to content

Commit a212fd9

Browse files
committed
Fix cancellation of ViewStore.suspend (#725)
* Fix cancellation of ViewStore.suspend * wip * Remove capture list
1 parent 410ff69 commit a212fd9

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,20 +215,20 @@ 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: Disposable?
221+
let cancellable = Box<Disposable?>(wrappedValue: nil)
218222
try? await withTaskCancellationHandler(
219-
handler: { [cancellable] in cancellable?.dispose() },
223+
handler: { cancellable.wrappedValue?.dispose() },
220224
operation: {
221225
try Task.checkCancellation()
222226
try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation<Void, Error>) in
223227
guard !Task.isCancelled else {
224228
continuation.resume(throwing: CancellationError())
225229
return
226230
}
227-
cancellable = self.produced.producer
231+
cancellable.wrappedValue = self.produced.producer
228232
.filter { !predicate($0) }
229233
.take(first: 1)
230234
.startWithValues { _ in
@@ -236,4 +240,12 @@ import SwiftUI
236240
)
237241
}
238242
}
243+
244+
private class Box<Value> {
245+
var wrappedValue: Value
246+
247+
init(wrappedValue: Value) {
248+
self.wrappedValue = wrappedValue
249+
}
250+
}
239251
#endif

0 commit comments

Comments
 (0)