Skip to content

Commit 2bf1be6

Browse files
Fix iOS 14 Cancellation Crash (#244)
* wip * wip Co-authored-by: Brandon Williams <[email protected]>
1 parent ea03db2 commit 2bf1be6

File tree

2 files changed

+57
-3
lines changed

2 files changed

+57
-3
lines changed

Sources/ComposableArchitecture/Effects/Cancellation.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,30 @@ extension Effect {
2727
/// canceled before starting this new one.
2828
/// - Returns: A new effect that is capable of being canceled by an identifier.
2929
public func cancellable(id: AnyHashable, cancelInFlight: Bool = false) -> Effect {
30+
// NB: This check intends to work around bugs over different versions of Combine
31+
#if swift(>=5.3) && !os(macOS)
32+
let effect = Deferred<
33+
Publishers.PrefixUntilOutput<Publishers.HandleEvents<Self>, PassthroughSubject<Void, Never>>
34+
> {
35+
let subject = PassthroughSubject<Void, Never>()
36+
lock.sync { subjects[id, default: []].append(subject) }
37+
let cleanup = {
38+
lock.sync {
39+
subjects[id]?.removeAll(where: { $0 === subject })
40+
if subjects[id]?.isEmpty == true {
41+
subjects[id] = nil
42+
}
43+
}
44+
}
45+
return self
46+
.handleEvents(
47+
receiveCompletion: { _ in cleanup() },
48+
receiveCancel: cleanup
49+
)
50+
.prefix(untilOutputFrom: subject)
51+
}
52+
.eraseToEffect()
53+
#else
3054
let effect = Deferred { () -> Publishers.HandleEvents<PassthroughSubject<Output, Failure>> in
3155
cancellablesLock.lock()
3256
defer { cancellablesLock.unlock() }
@@ -56,6 +80,7 @@ extension Effect {
5680
)
5781
}
5882
.eraseToEffect()
83+
#endif
5984

6085
return cancelInFlight ? .concatenate(.cancel(id: id), effect) : effect
6186
}
@@ -66,13 +91,26 @@ extension Effect {
6691
/// - Returns: A new effect that will cancel any currently in-flight effect with the given
6792
/// identifier.
6893
public static func cancel(id: AnyHashable) -> Effect {
94+
#if swift(>=5.3) && !os(macOS)
95+
return .fireAndForget {
96+
lock.sync {
97+
subjects[id]?.forEach { $0.send(()) }
98+
}
99+
}
100+
#else
69101
return .fireAndForget {
70102
cancellablesLock.sync {
71103
cancellationCancellables[id]?.forEach { $0.cancel() }
72104
}
73105
}
106+
#endif
74107
}
75108
}
76109

110+
#if swift(>=5.3) && !os(macOS)
111+
var subjects: [AnyHashable: [PassthroughSubject<Void, Never>]] = [:]
112+
let lock = NSRecursiveLock()
113+
#else
77114
var cancellationCancellables: [AnyHashable: Set<AnyCancellable>] = [:]
78115
let cancellablesLock = NSRecursiveLock()
116+
#endif

Tests/ComposableArchitectureTests/EffectCancellationTests.swift

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,11 @@ final class EffectCancellationTests: XCTestCase {
116116
.sink(receiveValue: { _ in })
117117
.store(in: &self.cancellables)
118118

119-
XCTAssertEqual([:], cancellationCancellables)
119+
#if swift(>=5.3) && !os(macOS)
120+
XCTAssertTrue(subjects.isEmpty)
121+
#else
122+
XCTAssertTrue(cancellationCancellables.isEmpty)
123+
#endif
120124
}
121125

122126
func testCancellablesCleanUp_OnCancel() {
@@ -132,7 +136,11 @@ final class EffectCancellationTests: XCTestCase {
132136
.sink(receiveValue: { _ in })
133137
.store(in: &self.cancellables)
134138

135-
XCTAssertEqual([:], cancellationCancellables)
139+
#if swift(>=5.3) && !os(macOS)
140+
XCTAssertTrue(subjects.isEmpty)
141+
#else
142+
XCTAssertTrue(cancellationCancellables.isEmpty)
143+
#endif
136144
}
137145

138146
func testDoubleCancellation() {
@@ -222,7 +230,11 @@ final class EffectCancellationTests: XCTestCase {
222230
.store(in: &self.cancellables)
223231
self.wait(for: [expectation], timeout: 999)
224232

233+
#if swift(>=5.3) && !os(macOS)
234+
XCTAssertTrue(subjects.isEmpty)
235+
#else
225236
XCTAssertTrue(cancellationCancellables.isEmpty)
237+
#endif
226238
}
227239

228240
func testNestedCancels() {
@@ -240,7 +252,11 @@ final class EffectCancellationTests: XCTestCase {
240252

241253
cancellables.removeAll()
242254

243-
XCTAssertEqual([:], cancellationCancellables)
255+
#if swift(>=5.3) && !os(macOS)
256+
XCTAssertTrue(subjects.isEmpty)
257+
#else
258+
XCTAssertTrue(cancellationCancellables.isEmpty)
259+
#endif
244260
}
245261

246262
func testSharedId() {

0 commit comments

Comments
 (0)