Skip to content

Commit 172d31b

Browse files
authored
Documentation and tests for the opt-in empty sentinel for sequence-of-streams operators. (#782)
* Update the documentation & rename `noUpstreamSentinal` to `emptySentinel`. * Add tests. * Fix indentation.
1 parent 3a925fa commit 172d31b

File tree

5 files changed

+197
-15
lines changed

5 files changed

+197
-15
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
}
2020
```
2121

22-
1. Joining an empty sequence of producers can now send an event on the joined signal producer by providing the `noUpstreamSentinel` parameter. This becomes relevant, when the sequence of producers is calculated from some other Signal and the signal resulting from the joined producers is observed. If no event is sent only when the producers sequence is empty, then the observer gets stalled and e.g. the ui won't update. (#774, kudos to @rocketnik)
22+
1. When `combineLatest` or `zip` over a sequence of `SignalProducer`s or `Property`s, you can now specify an optional `emptySentinel` parameter, which would be used when the sequence is empty.
23+
24+
This becomes relevant, when the sequence of producers is calculated from some other Signal and the signal resulting from the joined producers is observed. If no value is sent when the sequence is empty, the observer gets terminated silently, and, e.g., the UI would not be updated.
25+
26+
(#774, kudos to @rocketnik)
2327

2428
# 6.2.1
2529

Sources/Property.swift

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -318,13 +318,24 @@ extension PropertyProtocol {
318318

319319
/// Combines the values of all the given producers, in the manner described by
320320
/// `combineLatest(with:)`. Returns nil if the sequence is empty.
321-
public static func combineLatest<S: Sequence>(_ properties: S, noUpstreamSentinel: [S.Iterator.Element.Value]? = nil) -> Property<[S.Iterator.Element.Value]>? where S.Iterator.Element: PropertyProtocol {
321+
public static func combineLatest<S: Sequence>(_ properties: S) -> Property<[S.Iterator.Element.Value]>? where S.Iterator.Element: PropertyProtocol {
322322
let producers = properties.map { $0.producer }
323323
guard !producers.isEmpty else {
324324
return nil
325325
}
326326

327-
return Property(unsafeProducer: SignalProducer.combineLatest(producers, noUpstreamSentinel: noUpstreamSentinel))
327+
return Property(unsafeProducer: SignalProducer.combineLatest(producers))
328+
}
329+
330+
/// Combines the values of all the given `Property`s, in the manner described by
331+
/// `combineLatest(with:)`. If `properties` is empty, the resulting `Property` would have `emptySentinel` as its
332+
/// constant value.
333+
public static func combineLatest<S: Sequence>(
334+
_ properties: S,
335+
emptySentinel: [S.Iterator.Element.Value]
336+
) -> Property<[S.Iterator.Element.Value]> where S.Iterator.Element: PropertyProtocol {
337+
let producers = properties.map { $0.producer }
338+
return Property(unsafeProducer: SignalProducer.combineLatest(producers, emptySentinel: emptySentinel))
328339
}
329340

330341
/// Zips the values of all the given properties, in the manner described by
@@ -383,13 +394,24 @@ extension PropertyProtocol {
383394

384395
/// Zips the values of all the given properties, in the manner described by
385396
/// `zip(with:)`. Returns nil if the sequence is empty.
386-
public static func zip<S: Sequence>(_ properties: S, noUpstreamSentinel: [S.Iterator.Element.Value]? = nil) -> Property<[S.Iterator.Element.Value]>? where S.Iterator.Element: PropertyProtocol {
397+
public static func zip<S: Sequence>(_ properties: S) -> Property<[S.Iterator.Element.Value]>? where S.Iterator.Element: PropertyProtocol {
387398
let producers = properties.map { $0.producer }
388399
guard !producers.isEmpty else {
389400
return nil
390401
}
391402

392-
return Property(unsafeProducer: SignalProducer.zip(producers, noUpstreamSentinel: noUpstreamSentinel))
403+
return Property(unsafeProducer: SignalProducer.zip(producers))
404+
}
405+
406+
/// Combines the values of all the given `Property`s, in the manner described by
407+
/// `zip(with:)`. If `properties` is empty, the resulting `Property` would have `emptySentinel` as its
408+
/// constant value.
409+
public static func zip<S: Sequence>(
410+
_ properties: S,
411+
emptySentinel: [S.Iterator.Element.Value]
412+
) -> Property<[S.Iterator.Element.Value]> where S.Iterator.Element: PropertyProtocol {
413+
let producers = properties.map { $0.producer }
414+
return Property(unsafeProducer: SignalProducer.zip(producers, emptySentinel: emptySentinel))
393415
}
394416
}
395417

Sources/SignalProducer.swift

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2126,8 +2126,14 @@ extension SignalProducer {
21262126

21272127
/// Combines the values of all the given producers, in the manner described by
21282128
/// `combineLatest(with:)`. Will return an empty `SignalProducer` if the sequence is empty.
2129-
public static func combineLatest<S: Sequence>(_ producers: S, noUpstreamSentinel: [S.Iterator.Element.Value]? = nil) -> SignalProducer<[Value], Error> where S.Iterator.Element: SignalProducerConvertible, S.Iterator.Element.Value == Value, S.Iterator.Element.Error == Error {
2130-
return start(producers, noUpstreamSentinel: noUpstreamSentinel, Signal.combineLatest)
2129+
public static func combineLatest<S: Sequence>(_ producers: S) -> SignalProducer<[Value], Error> where S.Iterator.Element: SignalProducerConvertible, S.Iterator.Element.Value == Value, S.Iterator.Element.Error == Error {
2130+
return start(producers, Signal.combineLatest)
2131+
}
2132+
2133+
/// Combines the values of all the given producers, in the manner described by
2134+
/// `combineLatest(with:)`. If no producer is given, the resulting producer will constantly return `emptySentinel`.
2135+
public static func combineLatest<S: Sequence>(_ producers: S, emptySentinel: [S.Iterator.Element.Value]) -> SignalProducer<[Value], Error> where S.Iterator.Element: SignalProducerConvertible, S.Iterator.Element.Value == Value, S.Iterator.Element.Error == Error {
2136+
return start(producers, emptySentinel: emptySentinel, Signal.combineLatest)
21312137
}
21322138

21332139
/// Zips the values of all the given producers, in the manner described by
@@ -2204,21 +2210,33 @@ extension SignalProducer {
22042210

22052211
/// Zips the values of all the given producers, in the manner described by
22062212
/// `zipWith`. Will return an empty `SignalProducer` if the sequence is empty.
2207-
public static func zip<S: Sequence>(_ producers: S, noUpstreamSentinel: [S.Iterator.Element.Value]? = nil) -> SignalProducer<[Value], Error> where S.Iterator.Element: SignalProducerConvertible, S.Iterator.Element.Value == Value, S.Iterator.Element.Error == Error {
2208-
return start(producers, noUpstreamSentinel: noUpstreamSentinel, Signal.zip)
2213+
public static func zip<S: Sequence>(_ producers: S) -> SignalProducer<[Value], Error> where S.Iterator.Element: SignalProducerConvertible, S.Iterator.Element.Value == Value, S.Iterator.Element.Error == Error {
2214+
return start(producers, Signal.zip)
2215+
}
2216+
2217+
/// Combines the values of all the given producers, in the manner described by
2218+
/// `zip(with:)`. If no producer is given, the resulting producer will constantly return `emptySentinel`.
2219+
public static func zip<S: Sequence>(_ producers: S, emptySentinel: [S.Iterator.Element.Value]) -> SignalProducer<[Value], Error> where S.Iterator.Element: SignalProducerConvertible, S.Iterator.Element.Value == Value, S.Iterator.Element.Error == Error {
2220+
return start(producers, emptySentinel: emptySentinel, Signal.zip)
22092221
}
22102222

2211-
private static func start<S: Sequence>(_ producers: S, noUpstreamSentinel: [S.Iterator.Element.Value]?, _ transform: @escaping (AnySequence<Signal<Value, Error>>) -> Signal<[Value], Error>) -> SignalProducer<[Value], Error> where S.Iterator.Element: SignalProducerConvertible, S.Iterator.Element.Value == Value, S.Iterator.Element.Error == Error
2223+
private static func start<S: Sequence>(
2224+
_ producers: S,
2225+
emptySentinel: [S.Iterator.Element.Value]? = nil,
2226+
_ transform: @escaping (AnySequence<Signal<Value, Error>>) -> Signal<[Value], Error>
2227+
) -> SignalProducer<[Value], Error>
2228+
where S.Iterator.Element: SignalProducerConvertible, S.Iterator.Element.Value == Value, S.Iterator.Element.Error == Error
22122229
{
22132230
return SignalProducer<[Value], Error> { observer, lifetime in
22142231
let setup = producers.map {
22152232
(producer: $0.producer, pipe: Signal<Value, Error>.pipe())
22162233
}
22172234

22182235
guard !setup.isEmpty else {
2219-
if let noUpstreamSentinel = noUpstreamSentinel {
2220-
observer.send(value: noUpstreamSentinel)
2221-
}
2236+
if let emptySentinel = emptySentinel {
2237+
observer.send(value: emptySentinel)
2238+
}
2239+
22222240
observer.sendCompleted()
22232241
return
22242242
}
@@ -2743,12 +2761,14 @@ extension SignalProducer where Value == Bool {
27432761

27442762
/// Create a producer that computes a logical AND between the latest values of `booleans`.
27452763
///
2764+
/// If no producer is given in `booleans`, the resulting producer constantly emits `true`.
2765+
///
27462766
/// - parameters:
27472767
/// - booleans: A collection of boolean producers to be combined.
27482768
///
27492769
/// - returns: A producer that emits the logical AND results.
27502770
public static func all<BooleansCollection: Collection>(_ booleans: BooleansCollection) -> SignalProducer<Value, Error> where BooleansCollection.Element == SignalProducer<Value, Error> {
2751-
return combineLatest(booleans, noUpstreamSentinel: []).map { $0.reduce(true) { $0 && $1 } }
2771+
return combineLatest(booleans, emptySentinel: []).map { $0.reduce(true) { $0 && $1 } }
27522772
}
27532773

27542774
/// Create a producer that computes a logical AND between the latest values of `booleans`.
@@ -2785,16 +2805,20 @@ extension SignalProducer where Value == Bool {
27852805

27862806
/// Create a producer that computes a logical OR between the latest values of `booleans`.
27872807
///
2808+
/// If no producer is given in `booleans`, the resulting producer constantly emits `true`.
2809+
///
27882810
/// - parameters:
27892811
/// - booleans: A collection of boolean producers to be combined.
27902812
///
27912813
/// - returns: A producer that emits the logical OR results.
27922814
public static func any<BooleansCollection: Collection>(_ booleans: BooleansCollection) -> SignalProducer<Value, Error> where BooleansCollection.Element == SignalProducer<Value, Error> {
2793-
return combineLatest(booleans, noUpstreamSentinel: []).map { $0.reduce(false) { $0 || $1 } }
2815+
return combineLatest(booleans, emptySentinel: []).map { $0.reduce(false) { $0 || $1 } }
27942816
}
27952817

27962818
/// Create a producer that computes a logical OR between the latest values of `booleans`.
27972819
///
2820+
/// If no producer is given in `booleans`, the resulting producer constantly emits `true`.
2821+
///
27982822
/// - parameters:
27992823
/// - booleans: A collection of boolean producers to be combined.
28002824
///

Tests/ReactiveSwiftTests/PropertySpec.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,16 @@ class PropertySpec: QuickSpec {
854854
otherProperty = MutableProperty(initialOtherPropertyValue)
855855
}
856856

857+
it("should emit the empty sentinel when no property is given") {
858+
let property = Property<String>.combineLatest(
859+
EmptyCollection<Property<String>>(),
860+
emptySentinel: ["empty"]
861+
)
862+
863+
expect(property).toNot(beNil())
864+
expect(property.value) == ["empty"]
865+
}
866+
857867
it("should forward the latest values from both inputs") {
858868
let combinedProperty = property.combineLatest(with: otherProperty)
859869
var latest: (String, String)?
@@ -975,6 +985,16 @@ class PropertySpec: QuickSpec {
975985
otherProperty = MutableProperty(initialOtherPropertyValue)
976986
}
977987

988+
it("should emit the empty sentinel when no property is given") {
989+
let property = Property<String>.zip(
990+
EmptyCollection<Property<String>>(),
991+
emptySentinel: ["empty"]
992+
)
993+
994+
expect(property).toNot(beNil())
995+
expect(property.value) == ["empty"]
996+
}
997+
978998
it("should combine pairs") {
979999
var result: [String] = []
9801000

@@ -1710,6 +1730,15 @@ class PropertySpec: QuickSpec {
17101730
}
17111731

17121732
describe("all attribute") {
1733+
it("should return a property with constant true when no producer is given") {
1734+
let property = Property<Bool>.all(
1735+
EmptyCollection<Property<Bool>>()
1736+
)
1737+
1738+
expect(property).toNot(beNil())
1739+
expect(property.value) == true
1740+
}
1741+
17131742
it("should emit true when all properties contain the same value") {
17141743
let property1 = MutableProperty(true)
17151744
let property2 = MutableProperty(true)
@@ -1744,6 +1773,15 @@ class PropertySpec: QuickSpec {
17441773
}
17451774

17461775
describe("any attribute") {
1776+
it("should return a property with constant false when no producer is given") {
1777+
let property = Property<Bool>.any(
1778+
EmptyCollection<Property<Bool>>()
1779+
)
1780+
1781+
expect(property).toNot(beNil())
1782+
expect(property.value) == false
1783+
}
1784+
17471785
it("should emit true when at least one of the properties in array contains true") {
17481786
let property1 = MutableProperty(true)
17491787
let property2 = MutableProperty(false)

Tests/ReactiveSwiftTests/SignalProducerSpec.swift

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,30 @@ class SignalProducerSpec: QuickSpec {
952952
}
953953

954954
describe("combineLatest") {
955+
it("should emit the empty sentinel when no producer is given") {
956+
let producer = SignalProducer<String, Never>.combineLatest(
957+
EmptyCollection<SignalProducer<String, Never>>(),
958+
emptySentinel: ["empty"]
959+
)
960+
961+
var values = [[String]]()
962+
var isCompleted = false
963+
964+
producer.start { event in
965+
switch event {
966+
case let .value(value):
967+
values.append(value)
968+
case .completed:
969+
isCompleted = true
970+
case .interrupted, .failed:
971+
break
972+
}
973+
}
974+
975+
expect(values) == [["empty"]]
976+
expect(isCompleted) == true
977+
}
978+
955979
it("should combine the events to one array") {
956980
let (producerA, observerA) = SignalProducer<Int, Never>.pipe()
957981
let (producerB, observerB) = SignalProducer<Int, Never>.pipe()
@@ -1016,6 +1040,30 @@ class SignalProducerSpec: QuickSpec {
10161040
}
10171041

10181042
describe("zip") {
1043+
it("should emit the empty sentinel when no producer is given") {
1044+
let producer = SignalProducer<String, Never>.zip(
1045+
EmptyCollection<SignalProducer<String, Never>>(),
1046+
emptySentinel: ["empty"]
1047+
)
1048+
1049+
var values = [[String]]()
1050+
var isCompleted = false
1051+
1052+
producer.start { event in
1053+
switch event {
1054+
case let .value(value):
1055+
values.append(value)
1056+
case .completed:
1057+
isCompleted = true
1058+
case .interrupted, .failed:
1059+
break
1060+
}
1061+
}
1062+
1063+
expect(values) == [["empty"]]
1064+
expect(isCompleted) == true
1065+
}
1066+
10191067
it("should zip the events to one array") {
10201068
let producerA = SignalProducer<Int, Never>([ 1, 2 ])
10211069
let producerB = SignalProducer<Int, Never>([ 3, 4 ])
@@ -3403,6 +3451,29 @@ class SignalProducerSpec: QuickSpec {
34033451
}
34043452

34053453
describe("all attribute") {
3454+
it("should emit true when no producer is given") {
3455+
let producer = SignalProducer<Bool, Never>.all(
3456+
EmptyCollection<SignalProducer<Bool, Never>>()
3457+
)
3458+
3459+
var values = [Bool]()
3460+
var isCompleted = false
3461+
3462+
producer.start { event in
3463+
switch event {
3464+
case let .value(value):
3465+
values.append(value)
3466+
case .completed:
3467+
isCompleted = true
3468+
case .interrupted, .failed:
3469+
break
3470+
}
3471+
}
3472+
3473+
expect(values) == [true]
3474+
expect(isCompleted) == true
3475+
}
3476+
34063477
it("should emit true when all producers emit the same value") {
34073478
let producer1 = SignalProducer<Bool, Never> { observer, _ in
34083479
observer.send(value: true)
@@ -3510,6 +3581,29 @@ class SignalProducerSpec: QuickSpec {
35103581
}
35113582

35123583
describe("any attribute") {
3584+
it("should emit false when no producer is given") {
3585+
let producer = SignalProducer<Bool, Never>.any(
3586+
EmptyCollection<SignalProducer<Bool, Never>>()
3587+
)
3588+
3589+
var values = [Bool]()
3590+
var isCompleted = false
3591+
3592+
producer.start { event in
3593+
switch event {
3594+
case let .value(value):
3595+
values.append(value)
3596+
case .completed:
3597+
isCompleted = true
3598+
case .interrupted, .failed:
3599+
break
3600+
}
3601+
}
3602+
3603+
expect(values) == [false]
3604+
expect(isCompleted) == true
3605+
}
3606+
35133607
it("should emit true when at least one of the producers in array emits true") {
35143608
let producer1 = SignalProducer<Bool, Never> { observer, _ in
35153609
observer.send(value: true)

0 commit comments

Comments
 (0)