Fix iOS 14 Cancellation Crash (#244)

* wip

* wip

Co-authored-by: Brandon Williams <mbw234@gmail.com>
This commit is contained in:
Stephen Celis
2020-08-03 13:56:24 -04:00
committed by GitHub
parent ea03db2374
commit 2bf1be6bcf
2 changed files with 57 additions and 3 deletions

View File

@@ -27,6 +27,30 @@ extension Effect {
/// canceled before starting this new one.
/// - Returns: A new effect that is capable of being canceled by an identifier.
public func cancellable(id: AnyHashable, cancelInFlight: Bool = false) -> Effect {
// NB: This check intends to work around bugs over different versions of Combine
#if swift(>=5.3) && !os(macOS)
let effect = Deferred<
Publishers.PrefixUntilOutput<Publishers.HandleEvents<Self>, PassthroughSubject<Void, Never>>
> {
let subject = PassthroughSubject<Void, Never>()
lock.sync { subjects[id, default: []].append(subject) }
let cleanup = {
lock.sync {
subjects[id]?.removeAll(where: { $0 === subject })
if subjects[id]?.isEmpty == true {
subjects[id] = nil
}
}
}
return self
.handleEvents(
receiveCompletion: { _ in cleanup() },
receiveCancel: cleanup
)
.prefix(untilOutputFrom: subject)
}
.eraseToEffect()
#else
let effect = Deferred { () -> Publishers.HandleEvents<PassthroughSubject<Output, Failure>> in
cancellablesLock.lock()
defer { cancellablesLock.unlock() }
@@ -56,6 +80,7 @@ extension Effect {
)
}
.eraseToEffect()
#endif
return cancelInFlight ? .concatenate(.cancel(id: id), effect) : effect
}
@@ -66,13 +91,26 @@ extension Effect {
/// - Returns: A new effect that will cancel any currently in-flight effect with the given
/// identifier.
public static func cancel(id: AnyHashable) -> Effect {
#if swift(>=5.3) && !os(macOS)
return .fireAndForget {
lock.sync {
subjects[id]?.forEach { $0.send(()) }
}
}
#else
return .fireAndForget {
cancellablesLock.sync {
cancellationCancellables[id]?.forEach { $0.cancel() }
}
}
#endif
}
}
#if swift(>=5.3) && !os(macOS)
var subjects: [AnyHashable: [PassthroughSubject<Void, Never>]] = [:]
let lock = NSRecursiveLock()
#else
var cancellationCancellables: [AnyHashable: Set<AnyCancellable>] = [:]
let cancellablesLock = NSRecursiveLock()
#endif

View File

@@ -116,7 +116,11 @@ final class EffectCancellationTests: XCTestCase {
.sink(receiveValue: { _ in })
.store(in: &self.cancellables)
XCTAssertEqual([:], cancellationCancellables)
#if swift(>=5.3) && !os(macOS)
XCTAssertTrue(subjects.isEmpty)
#else
XCTAssertTrue(cancellationCancellables.isEmpty)
#endif
}
func testCancellablesCleanUp_OnCancel() {
@@ -132,7 +136,11 @@ final class EffectCancellationTests: XCTestCase {
.sink(receiveValue: { _ in })
.store(in: &self.cancellables)
XCTAssertEqual([:], cancellationCancellables)
#if swift(>=5.3) && !os(macOS)
XCTAssertTrue(subjects.isEmpty)
#else
XCTAssertTrue(cancellationCancellables.isEmpty)
#endif
}
func testDoubleCancellation() {
@@ -222,7 +230,11 @@ final class EffectCancellationTests: XCTestCase {
.store(in: &self.cancellables)
self.wait(for: [expectation], timeout: 999)
#if swift(>=5.3) && !os(macOS)
XCTAssertTrue(subjects.isEmpty)
#else
XCTAssertTrue(cancellationCancellables.isEmpty)
#endif
}
func testNestedCancels() {
@@ -240,7 +252,11 @@ final class EffectCancellationTests: XCTestCase {
cancellables.removeAll()
XCTAssertEqual([:], cancellationCancellables)
#if swift(>=5.3) && !os(macOS)
XCTAssertTrue(subjects.isEmpty)
#else
XCTAssertTrue(cancellationCancellables.isEmpty)
#endif
}
func testSharedId() {