Skip to content

Commit 033fac9

Browse files
authored
Merge pull request #463 from ReactiveCocoa/auto-interrupt
Send `interrupted` when an input observer deinitializes.
2 parents d635059 + 2f906e8 commit 033fac9

File tree

8 files changed

+131
-33
lines changed

8 files changed

+131
-33
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# master
22
*Please add new entries at the top.*
33

4+
1. If the input observer of a `Signal` deinitializes while the `Signal` has not yet terminated, an `interrupted` event would now be automatically sent. (#463, kudos to @andersio)
5+
46
1. `ValidationResult` and `ValidatorOutput` have been renamed to `ValidatingProperty.Result` and `ValidatingProperty.Decision`, respectively. (#443)
57

68
1. Mitigated a race condition related to ARC in the `Signal` internal. (#456, kudos to @andersio)

Sources/Observer.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,29 @@ extension Signal {
1515
/// An action that will be performed upon arrival of the event.
1616
public let action: Action
1717

18+
/// Whether the observer should send an `interrupted` event as it deinitializes.
19+
private let interruptsOnDeinit: Bool
20+
21+
/// An initializer that accepts a closure accepting an event for the
22+
/// observer.
23+
///
24+
/// - parameters:
25+
/// - action: A closure to lift over received event.
26+
/// - interruptsOnDeinit: `true` if the observer should send an `interrupted`
27+
/// event as it deinitializes. `false` otherwise.
28+
internal init(action: @escaping Action, interruptsOnDeinit: Bool) {
29+
self.action = action
30+
self.interruptsOnDeinit = interruptsOnDeinit
31+
}
32+
1833
/// An initializer that accepts a closure accepting an event for the
1934
/// observer.
2035
///
2136
/// - parameters:
2237
/// - action: A closure to lift over received event.
2338
public init(_ action: @escaping Action) {
2439
self.action = action
40+
self.interruptsOnDeinit = false
2541
}
2642

2743
/// An initializer that accepts closures for different event types.
@@ -68,6 +84,15 @@ extension Signal {
6884
}
6985
}
7086

87+
deinit {
88+
if interruptsOnDeinit {
89+
// Since `Signal` would ensure that only one terminal event would ever be
90+
// sent for any given `Signal`, we do not need to assert any condition
91+
// here.
92+
action(.interrupted)
93+
}
94+
}
95+
7196
/// Puts a `value` event into `self`.
7297
///
7398
/// - parameters:

Sources/Signal.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public final class Signal<Value, Error: Swift.Error> {
7070
disposable = SerialDisposable()
7171

7272
// The generator observer retains the `Signal` core.
73-
disposable.inner = generator(Observer(self.send))
73+
disposable.inner = generator(Observer(action: self.send, interruptsOnDeinit: true))
7474
}
7575

7676
private func send(_ event: Event) {
@@ -376,7 +376,11 @@ public final class Signal<Value, Error: Swift.Error> {
376376
extension Signal {
377377
/// A Signal that never sends any events to its observers.
378378
public static var never: Signal {
379-
return self.init { _ in nil }
379+
return self.init { observer in
380+
// If `observer` deinitializes, the `Signal` would interrupt which is
381+
// undesirable for `Signal.never`.
382+
return AnyDisposable { _ = observer }
383+
}
380384
}
381385

382386
/// A Signal that completes immediately without emitting any value.

Tests/ReactiveSwiftTests/PropertySpec.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1680,17 +1680,19 @@ class PropertySpec: QuickSpec {
16801680
}
16811681

16821682
it("should tear down the binding when the property deallocates") {
1683-
let (signal, _) = Signal<String, NoError>.pipe()
1683+
let (signal, observer) = Signal<String, NoError>.pipe()
16841684
let signalProducer = SignalProducer(signal)
16851685

16861686
var mutableProperty: MutableProperty<String>? = MutableProperty(initialPropertyValue)
16871687

1688-
var isDisposed = false
1689-
mutableProperty! <~ signalProducer.on(disposed: { isDisposed = true })
1690-
expect(isDisposed) == false
1688+
withExtendedLifetime(observer) {
1689+
var isDisposed = false
1690+
mutableProperty! <~ signalProducer.on(disposed: { isDisposed = true })
1691+
expect(isDisposed) == false
16911692

1692-
mutableProperty = nil
1693-
expect(isDisposed) == true
1693+
mutableProperty = nil
1694+
expect(isDisposed) == true
1695+
}
16941696
}
16951697
}
16961698

Tests/ReactiveSwiftTests/SignalLifetimeSpec.swift

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,44 @@ class SignalLifetimeSpec: QuickSpec {
2222
testScheduler = TestScheduler()
2323
}
2424

25+
it("should automatically interrupt if the input observer is not retained") {
26+
let disposable = AnyDisposable()
27+
var outerSignal: Signal<Never, NoError>!
28+
29+
func scope() {
30+
let (signal, observer) = Signal<Never, NoError>.pipe(disposable: disposable)
31+
outerSignal = signal
32+
33+
expect(disposable.isDisposed) == false
34+
}
35+
36+
scope()
37+
expect(disposable.isDisposed) == true
38+
}
39+
40+
41+
it("should automatically interrupt if the input observer is not retained, even if there are still one or more active observer") {
42+
let disposable = AnyDisposable()
43+
var isInterrupted = false
44+
var outerSignal: Signal<Never, NoError>!
45+
46+
func scope() {
47+
let (signal, observer) = Signal<Never, NoError>.pipe(disposable: disposable)
48+
outerSignal = signal
49+
50+
signal.observeInterrupted {
51+
isInterrupted = true
52+
}
53+
54+
expect(isInterrupted) == false
55+
expect(disposable.isDisposed) == false
56+
}
57+
58+
scope()
59+
expect(isInterrupted) == true
60+
expect(disposable.isDisposed) == true
61+
}
62+
2563
it("should be disposed of if it does not have any observers") {
2664
var isDisposed = false
2765

@@ -35,20 +73,29 @@ class SignalLifetimeSpec: QuickSpec {
3573

3674
it("should be disposed of if no one retains it") {
3775
var isDisposed = false
38-
var signal: Signal<AnyObject, NoError>? = Signal { _ in nil }.on(disposed: { isDisposed = true })
76+
var observer: Signal<AnyObject, NoError>.Observer!
77+
var signal: Signal<AnyObject, NoError>? = Signal
78+
.init { innerObserver in
79+
observer = innerObserver
80+
return nil
81+
}
82+
.on(disposed: { isDisposed = true })
83+
3984
weak var weakSignal = signal
4085

41-
expect(weakSignal).toNot(beNil())
42-
expect(isDisposed) == false
86+
withExtendedLifetime(observer) {
87+
expect(weakSignal).toNot(beNil())
88+
expect(isDisposed) == false
4389

44-
var reference = signal
45-
signal = nil
46-
expect(weakSignal).toNot(beNil())
47-
expect(isDisposed) == false
90+
var reference = signal
91+
signal = nil
92+
expect(weakSignal).toNot(beNil())
93+
expect(isDisposed) == false
4894

49-
reference = nil
50-
expect(weakSignal).to(beNil())
51-
expect(isDisposed) == true
95+
reference = nil
96+
expect(weakSignal).to(beNil())
97+
expect(isDisposed) == true
98+
}
5299
}
53100

54101
it("should be disposed of when the signal shell has deinitialized with no active observer regardless of whether the generator observer is retained or not") {

Tests/ReactiveSwiftTests/SignalProducerLiftingSpec.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1430,12 +1430,14 @@ class SignalProducerLiftingSpec: QuickSpec {
14301430
}
14311431

14321432
var sampledProducer: SignalProducer<Payload, NoError>!
1433+
var samplerObserver: Signal<(), NoError>.Observer!
14331434
var observer: Signal<Payload, NoError>.Observer!
14341435

14351436
beforeEach {
14361437
let (producer, incomingObserver) = SignalProducer<Payload, NoError>.pipe()
1437-
let (sampler, _) = Signal<(), NoError>.pipe()
1438+
let (sampler, _samplerObserver) = Signal<(), NoError>.pipe()
14381439
sampledProducer = producer.sample(on: sampler)
1440+
samplerObserver = _samplerObserver
14391441
observer = incomingObserver
14401442
}
14411443

Tests/ReactiveSwiftTests/SignalProducerSpec.swift

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,18 +1049,20 @@ class SignalProducerSpec: QuickSpec {
10491049
}
10501050

10511051
it("should attach event handlers for disposal") {
1052-
let (baseProducer, _) = SignalProducer<Int, TestError>.pipe()
1052+
let (baseProducer, observer) = SignalProducer<Int, TestError>.pipe()
10531053

1054-
var disposed: Bool = false
1054+
withExtendedLifetime(observer) {
1055+
var disposed: Bool = false
10551056

1056-
let producer = baseProducer
1057-
.on(disposed: { disposed = true })
1057+
let producer = baseProducer
1058+
.on(disposed: { disposed = true })
10581059

1059-
let disposable = producer.start()
1060+
let disposable = producer.start()
10601061

1061-
expect(disposed) == false
1062-
disposable.dispose()
1063-
expect(disposed) == true
1062+
expect(disposed) == false
1063+
disposable.dispose()
1064+
expect(disposed) == true
1065+
}
10641066
}
10651067

10661068
it("should invoke the `started` action of the inner producer first") {
@@ -1323,12 +1325,15 @@ class SignalProducerSpec: QuickSpec {
13231325
var completeB: (() -> Void)!
13241326
var sendB: (() -> Void)!
13251327

1328+
var outerObserver: Signal<SignalProducer<Int, NoError>, NoError>.Observer!
13261329
var outerCompleted = false
13271330

13281331
var recv = [Int]()
13291332

13301333
beforeEach {
1331-
let (outerProducer, outerObserver) = SignalProducer<SignalProducer<Int, NoError>, NoError>.pipe()
1334+
let (outerProducer, _outerObserver) = SignalProducer<SignalProducer<Int, NoError>, NoError>.pipe()
1335+
outerObserver = _outerObserver
1336+
13321337
let (producerA, observerA) = SignalProducer<Int, NoError>.pipe()
13331338
let (producerB, observerB) = SignalProducer<Int, NoError>.pipe()
13341339

@@ -1358,6 +1363,15 @@ class SignalProducerSpec: QuickSpec {
13581363
outerObserver.sendCompleted()
13591364
}
13601365

1366+
afterEach {
1367+
(completeA, completeB) = (nil, nil)
1368+
(sendA, sendB) = (nil, nil)
1369+
outerObserver = nil
1370+
1371+
outerCompleted = false
1372+
recv = []
1373+
}
1374+
13611375
it("should forward values from any inner signals") {
13621376
sendA()
13631377
sendA()

Tests/ReactiveSwiftTests/SignalSpec.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -260,19 +260,21 @@ class SignalSpec: QuickSpec {
260260

261261
context("memory") {
262262
it("should not crash allocating memory with a few observers") {
263-
let (signal, _) = Signal<Int, NoError>.pipe()
263+
let (signal, observer) = Signal<Int, NoError>.pipe()
264264

265265
#if os(Linux)
266266
func autoreleasepool(invoking code: () -> Void) {
267267
code()
268268
}
269269
#endif
270270

271-
for _ in 0..<50 {
272-
autoreleasepool {
273-
let disposable = signal.observe { _ in }
271+
withExtendedLifetime(observer) {
272+
for _ in 0..<50 {
273+
autoreleasepool {
274+
let disposable = signal.observe { _ in }
274275

275-
disposable!.dispose()
276+
disposable!.dispose()
277+
}
276278
}
277279
}
278280
}

0 commit comments

Comments
 (0)