Skip to content

Commit 56d51e9

Browse files
authored
Add take(until:) operator (#839)
Forward any values until `shouldContinue` returns `false`, at which point the returned signal forwards the last value and then it completes. This is equivalent to `take(while:)`, except it also forwards the last value that failed the check.
1 parent 8c5bb1e commit 56d51e9

File tree

8 files changed

+190
-2
lines changed

8 files changed

+190
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
When debugging your application, the call stacks involving ReactiveSwift may now look cleaner, without the clutter of compiler-generated reabstraction thunks. See #799 for an example.
2828

29+
1. New operator `SignalProducer.take(until:)` that forwards any values until `shouldContinue` returns `false`. Equivalent to `take(while:)`, except it also forwards the last value that failed the check. (#839, kudos to @nachosoto)
30+
2931
# 6.6.1
3032
1. Updated Carthage xcconfig dependency to 1.1 for proper building arm64 macOS variants. (#826, kudos to @MikeChugunov)
3133

ReactiveSwift.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
4A0E11041D2A95200065D310 /* LifetimeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0E11031D2A95200065D310 /* LifetimeSpec.swift */; };
2020
4A0E11051D2A95200065D310 /* LifetimeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0E11031D2A95200065D310 /* LifetimeSpec.swift */; };
2121
4A0E11061D2A95200065D310 /* LifetimeSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A0E11031D2A95200065D310 /* LifetimeSpec.swift */; };
22+
5792972826DE7623007A9F64 /* TakeUntil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5792972726DE7623007A9F64 /* TakeUntil.swift */; };
23+
5792972926DE7623007A9F64 /* TakeUntil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5792972726DE7623007A9F64 /* TakeUntil.swift */; };
24+
5792972A26DE7623007A9F64 /* TakeUntil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5792972726DE7623007A9F64 /* TakeUntil.swift */; };
25+
5792972B26DE7623007A9F64 /* TakeUntil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5792972726DE7623007A9F64 /* TakeUntil.swift */; };
2226
579504331BB8A34200A5E482 /* BagSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C312EF19EF2A7700984962 /* BagSpec.swift */; };
2327
579504341BB8A34300A5E482 /* BagSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C312EF19EF2A7700984962 /* BagSpec.swift */; };
2428
57A4D1B11BA13D7A00F7D4B1 /* Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = D871D69E1B3B29A40070F16C /* Optional.swift */; };
@@ -393,6 +397,7 @@
393397
4A0AB6711DC28EFF00AA1E81 /* ReactiveExtensionsSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactiveExtensionsSpec.swift; sourceTree = "<group>"; };
394398
4A0E10FE1D2A92720065D310 /* Lifetime.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Lifetime.swift; sourceTree = "<group>"; };
395399
4A0E11031D2A95200065D310 /* LifetimeSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LifetimeSpec.swift; sourceTree = "<group>"; };
400+
5792972726DE7623007A9F64 /* TakeUntil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TakeUntil.swift; sourceTree = "<group>"; };
396401
57A4D2411BA13D7A00F7D4B1 /* ReactiveSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReactiveSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; };
397402
57A4D2441BA13F9700F7D4B1 /* tvOS-Application.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "tvOS-Application.xcconfig"; sourceTree = "<group>"; };
398403
57A4D2451BA13F9700F7D4B1 /* tvOS-Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "tvOS-Base.xcconfig"; sourceTree = "<group>"; };
@@ -578,6 +583,7 @@
578583
9A2D5C9E259F8059005682ED /* TakeFirst.swift */,
579584
9A2D5CAD259F8112005682ED /* TakeLast.swift */,
580585
9A2D5CB7259F8199005682ED /* TakeWhile.swift */,
586+
5792972726DE7623007A9F64 /* TakeUntil.swift */,
581587
9A2D5CC1259F81FC005682ED /* SkipFirst.swift */,
582588
9A2D5CCB259F8263005682ED /* SkipWhile.swift */,
583589
9A2D5C4E259F7B21005682ED /* MapError.swift */,
@@ -1051,6 +1057,7 @@
10511057
57A4D1B81BA13D7A00F7D4B1 /* Scheduler.swift in Sources */,
10521058
9A9100E21E0E6E680093E346 /* ValidatingProperty.swift in Sources */,
10531059
9A2D5CF2259F85AE005682ED /* SkipRepeats.swift in Sources */,
1060+
5792972B26DE7623007A9F64 /* TakeUntil.swift in Sources */,
10541061
9A2D5CBB259F8199005682ED /* TakeWhile.swift in Sources */,
10551062
57A4D1B91BA13D7A00F7D4B1 /* Action.swift in Sources */,
10561063
57A4D1BA1BA13D7A00F7D4B1 /* Property.swift in Sources */,
@@ -1153,6 +1160,7 @@
11531160
9AFA491324E9A196003D263C /* Map.swift in Sources */,
11541161
9A2D5CFB259F8634005682ED /* UniqueValues.swift in Sources */,
11551162
9A2D5C65259F7B47005682ED /* MaterializeAsResult.swift in Sources */,
1163+
5792972A26DE7623007A9F64 /* TakeUntil.swift in Sources */,
11561164
9A2D5D55259FA000005682ED /* Throttle.swift in Sources */,
11571165
9A67963D1F6059430058C5B4 /* UninhabitedTypeGuards.swift in Sources */,
11581166
9A2D5D5F259FA0DD005682ED /* Debounce.swift in Sources */,
@@ -1196,6 +1204,7 @@
11961204
9A9100DF1E0E6E620093E346 /* ValidatingProperty.swift in Sources */,
11971205
EBCC7DBC1BBF010C00A2AE92 /* Signal.Observer.swift in Sources */,
11981206
9A2D5CEF259F85AE005682ED /* SkipRepeats.swift in Sources */,
1207+
5792972826DE7623007A9F64 /* TakeUntil.swift in Sources */,
11991208
9A2D5CB8259F8199005682ED /* TakeWhile.swift in Sources */,
12001209
D03B4A3D19F4C39A009E02AC /* FoundationExtensions.swift in Sources */,
12011210
9A090C141DA0309E00EE97CA /* Reactive.swift in Sources */,
@@ -1298,6 +1307,7 @@
12981307
9AFA491224E9A196003D263C /* Map.swift in Sources */,
12991308
9A2D5CFA259F8634005682ED /* UniqueValues.swift in Sources */,
13001309
9A2D5C64259F7B47005682ED /* MaterializeAsResult.swift in Sources */,
1310+
5792972926DE7623007A9F64 /* TakeUntil.swift in Sources */,
13011311
9A2D5D54259FA000005682ED /* Throttle.swift in Sources */,
13021312
9A67963C1F6059420058C5B4 /* UninhabitedTypeGuards.swift in Sources */,
13031313
9A2D5D5E259FA0DD005682ED /* Debounce.swift in Sources */,

Sources/Event.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,12 @@ extension Signal.Event {
290290
Operators.TakeWhile(downstream: downstream, shouldContinue: shouldContinue)
291291
}
292292
}
293+
294+
internal static func take(until shouldContinue: @escaping (Value) -> Bool) -> Transformation<Value, Error> {
295+
return { downstream, _ in
296+
Operators.TakeUntil(downstream: downstream, shouldContinue: shouldContinue)
297+
}
298+
}
293299

294300
internal static func skip(first count: Int) -> Transformation<Value, Error> {
295301
return { downstream, _ in

Sources/Observers/TakeUntil.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
extension Operators {
2+
internal final class TakeUntil<Value, Error: Swift.Error>: Observer<Value, Error> {
3+
let downstream: Observer<Value, Error>
4+
let shouldContinue: (Value) -> Bool
5+
6+
init(downstream: Observer<Value, Error>, shouldContinue: @escaping (Value) -> Bool) {
7+
self.downstream = downstream
8+
self.shouldContinue = shouldContinue
9+
}
10+
11+
override func receive(_ value: Value) {
12+
downstream.receive(value)
13+
14+
if !shouldContinue(value) {
15+
downstream.terminate(.completed)
16+
}
17+
}
18+
19+
override func terminate(_ termination: Termination<Error>) {
20+
downstream.terminate(termination)
21+
}
22+
}
23+
}

Sources/Signal.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,9 +1595,23 @@ extension Signal {
15951595
/// continue.
15961596
///
15971597
/// - returns: A signal which conditionally forwards values from `self`.
1598+
/// - seealso: `take(until:)`
15981599
public func take(while shouldContinue: @escaping (Value) -> Bool) -> Signal<Value, Error> {
15991600
return flatMapEvent(Signal.Event.take(while: shouldContinue))
16001601
}
1602+
1603+
/// Forward any values from `self` until `shouldContinue` returns `false`, at which
1604+
/// point the returned signal forwards the last value and then it completes.
1605+
/// This is equivalent to `take(while:)`, except it also forwards the last value that failed the check.
1606+
///
1607+
/// - parameters:
1608+
/// - shouldContinue: A closure to determine whether the forwarding of values should
1609+
/// continue.
1610+
///
1611+
/// - returns: A signal which conditionally forwards values from `self`.
1612+
public func take(until shouldContinue: @escaping (Value) -> Bool) -> Signal<Value, Error> {
1613+
return flatMapEvent(Signal.Event.take(until: shouldContinue))
1614+
}
16011615
}
16021616

16031617
extension Signal {

Sources/SignalProducer.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,6 +1630,19 @@ extension SignalProducer {
16301630
return core.flatMapEvent(Signal.Event.take(last: count))
16311631
}
16321632

1633+
/// Forward any values from `self` until `shouldContinue` returns `false`, at which
1634+
/// point the returned signal forwards the last value and then it completes.
1635+
/// This is equivalent to `take(while:)`, except it also forwards the last value that failed the check.
1636+
///
1637+
/// - parameters:
1638+
/// - shouldContinue: A closure to determine whether the forwarding of values should
1639+
/// continue.
1640+
///
1641+
/// - returns: A signal which conditionally forwards values from `self`.
1642+
public func take(until shouldContinue: @escaping (Value) -> Bool) -> SignalProducer<Value, Error> {
1643+
return core.flatMapEvent(Signal.Event.take(until: shouldContinue))
1644+
}
1645+
16331646
/// Forward any values from `self` until `shouldContinue` returns `false`, at which
16341647
/// point the produced `Signal` would complete.
16351648
///

Tests/ReactiveSwiftTests/SignalProducerLiftingSpec.swift

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1148,7 +1148,7 @@ class SignalProducerLiftingSpec: QuickSpec {
11481148

11491149
beforeEach {
11501150
let (baseProducer, incomingObserver) = SignalProducer<Int, Never>.pipe()
1151-
producer = baseProducer.take { $0 <= 4 }
1151+
producer = baseProducer.take(while: { $0 <= 4 })
11521152
observer = incomingObserver
11531153
}
11541154

@@ -1198,6 +1198,66 @@ class SignalProducerLiftingSpec: QuickSpec {
11981198
expect(completed) == true
11991199
}
12001200
}
1201+
1202+
describe("takeUntil") {
1203+
var producer: SignalProducer<Int, Never>!
1204+
var observer: Signal<Int, Never>.Observer!
1205+
1206+
beforeEach {
1207+
let (baseProducer, incomingObserver) = SignalProducer<Int, Never>.pipe()
1208+
producer = baseProducer.take(until: { $0 <= 4 })
1209+
observer = incomingObserver
1210+
}
1211+
1212+
it("should take until the predicate is true") {
1213+
var latestValue: Int!
1214+
var completed = false
1215+
1216+
producer.start { event in
1217+
switch event {
1218+
case let .value(value):
1219+
latestValue = value
1220+
case .completed:
1221+
completed = true
1222+
case .failed, .interrupted:
1223+
break
1224+
}
1225+
}
1226+
1227+
for value in -1...4 {
1228+
observer.send(value: value)
1229+
expect(latestValue) == value
1230+
expect(completed) == false
1231+
}
1232+
1233+
observer.send(value: 5)
1234+
expect(latestValue) == 5
1235+
expect(completed) == true
1236+
1237+
observer.send(value: 6)
1238+
expect(latestValue) == 5
1239+
}
1240+
1241+
it("should take and then complete if the predicate starts false") {
1242+
var latestValue: Int?
1243+
var completed = false
1244+
1245+
producer.start { event in
1246+
switch event {
1247+
case let .value(value):
1248+
latestValue = value
1249+
case .completed:
1250+
completed = true
1251+
case .failed, .interrupted:
1252+
break
1253+
}
1254+
}
1255+
1256+
observer.send(value: 5)
1257+
expect(latestValue) == 5
1258+
expect(completed) == true
1259+
}
1260+
}
12011261

12021262
describe("observeOn") {
12031263
it("should send events on the given scheduler") {

Tests/ReactiveSwiftTests/SignalSpec.swift

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1644,7 +1644,7 @@ class SignalSpec: QuickSpec {
16441644

16451645
beforeEach {
16461646
let (baseSignal, incomingObserver) = Signal<Int, Never>.pipe()
1647-
signal = baseSignal.take { $0 <= 4 }
1647+
signal = baseSignal.take(while: { $0 <= 4 })
16481648
observer = incomingObserver
16491649
}
16501650

@@ -1694,6 +1694,66 @@ class SignalSpec: QuickSpec {
16941694
expect(completed) == true
16951695
}
16961696
}
1697+
1698+
describe("takeUntil") {
1699+
var signal: Signal<Int, Never>!
1700+
var observer: Signal<Int, Never>.Observer!
1701+
1702+
beforeEach {
1703+
let (baseSignal, incomingObserver) = Signal<Int, Never>.pipe()
1704+
signal = baseSignal.take(until: { $0 <= 4 })
1705+
observer = incomingObserver
1706+
}
1707+
1708+
it("should take until the predicate is true") {
1709+
var latestValue: Int!
1710+
var completed = false
1711+
1712+
signal.observe { event in
1713+
switch event {
1714+
case let .value(value):
1715+
latestValue = value
1716+
case .completed:
1717+
completed = true
1718+
default:
1719+
break
1720+
}
1721+
}
1722+
1723+
for value in -1...4 {
1724+
observer.send(value: value)
1725+
expect(latestValue) == value
1726+
expect(completed) == false
1727+
}
1728+
1729+
observer.send(value: 5)
1730+
expect(latestValue) == 5
1731+
expect(completed) == true
1732+
1733+
observer.send(value: 6)
1734+
expect(latestValue) == 5
1735+
}
1736+
1737+
it("should take and then complete if the predicate starts false") {
1738+
var latestValue: Int?
1739+
var completed = false
1740+
1741+
signal.observe { event in
1742+
switch event {
1743+
case let .value(value):
1744+
latestValue = value
1745+
case .completed:
1746+
completed = true
1747+
default:
1748+
break
1749+
}
1750+
}
1751+
1752+
observer.send(value: 5)
1753+
expect(latestValue) == 5
1754+
expect(completed) == true
1755+
}
1756+
}
16971757

16981758
describe("observeOn") {
16991759
it("should send events on the given scheduler") {

0 commit comments

Comments
 (0)