Skip to content

Commit 4c37c91

Browse files
authored
Repeater timing (#9)
1 parent c54ebcc commit 4c37c91

File tree

5 files changed

+124
-23
lines changed

5 files changed

+124
-23
lines changed

Sources/QLoop/QLAnchor+ConvenienceInit.swift

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,46 @@ public extension QLAnchor {
1212
onError: QLAnchor.emptyErr)
1313
}
1414

15-
convenience init(repeaters: QLAnchor...) {
15+
convenience init(earlyRepeaters: QLAnchor...) {
1616
self.init(echoFilter: QLAnchor.DefaultEchoFilter,
17-
repeaters: repeaters)
17+
earlyRepeaters: earlyRepeaters,
18+
lateRepeaters: [])
19+
}
20+
21+
convenience init(lateRepeaters: QLAnchor...) {
22+
self.init(echoFilter: QLAnchor.DefaultEchoFilter,
23+
earlyRepeaters: [],
24+
lateRepeaters: lateRepeaters)
25+
}
26+
27+
convenience init(earlyRepeaters: [QLAnchor],
28+
lateRepeaters: [QLAnchor]) {
29+
self.init(echoFilter: QLAnchor.DefaultEchoFilter,
30+
earlyRepeaters: earlyRepeaters,
31+
lateRepeaters: lateRepeaters)
32+
}
33+
34+
convenience init(echoFilter: @escaping EchoFilter,
35+
earlyRepeaters: QLAnchor...) {
36+
self.init(echoFilter: echoFilter,
37+
earlyRepeaters: earlyRepeaters,
38+
lateRepeaters: [])
1839
}
1940

2041
convenience init(echoFilter: @escaping EchoFilter,
21-
repeaters: QLAnchor...) {
42+
lateRepeaters: QLAnchor...) {
2243
self.init(echoFilter: echoFilter,
23-
repeaters: repeaters)
44+
earlyRepeaters: [],
45+
lateRepeaters: lateRepeaters)
2446
}
2547

2648
convenience init(echoFilter: @escaping EchoFilter,
27-
repeaters: [QLAnchor]) {
49+
earlyRepeaters: [QLAnchor],
50+
lateRepeaters: [QLAnchor]) {
2851
self.init(onChange: QLAnchor.emptyIn,
2952
onError: QLAnchor.emptyErr)
30-
self.repeaters = repeaters
53+
self.addRepeaters(earlyRepeaters, timing: .early)
54+
self.addRepeaters(lateRepeaters, timing: .late)
3155
self.echoFilter = echoFilter
3256
}
3357
}

Sources/QLoop/QLAnchor.swift

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,28 @@ public final class QLAnchor<Input>: AnyAnchor {
1111
public typealias EchoFilter = (Input?, QLAnchor<Input>) -> (Bool)
1212
internal static var DefaultEchoFilter: EchoFilter { return { _, _ in return true } }
1313

14+
public enum Timing {
15+
case early, late
16+
}
17+
1418
internal final class Repeater {
19+
1520
weak var anchor: QLAnchor?
16-
init(_ anchor: QLAnchor) {
21+
22+
let timing: Timing
23+
24+
init(_ anchor: QLAnchor, timing: Timing) {
25+
self.timing = timing
1726
self.anchor = anchor
1827
}
19-
func echo(value: Input?, filter: EchoFilter) {
28+
29+
func echo(value: Input?, filter: EchoFilter, timing: Timing) {
2030
if let repeater = self.anchor,
2131
filter(value, repeater) {
2232
repeater.value = value
2333
}
2434
}
35+
2536
func echo(error: Error) {
2637
anchor?.error = error
2738
}
@@ -42,9 +53,13 @@ public final class QLAnchor<Input>: AnyAnchor {
4253

4354
public var inputSegment: AnySegment?
4455

45-
public var repeaters: [QLAnchor] {
46-
get { return _repeaters.compactMap { $0.anchor } }
47-
set { self._repeaters = newValue.map { Repeater($0) } }
56+
public func addRepeaters(_ repeaters: [QLAnchor], timing: Timing) {
57+
let repeaters = repeaters.map { Repeater($0, timing: timing) }
58+
self._repeaters.append(contentsOf: repeaters)
59+
}
60+
61+
public func getRepeaters(timing: Timing) -> [QLAnchor] {
62+
return _repeaters.compactMap { ($0.timing == timing) ? $0.anchor : nil }
4863
}
4964

5065
internal var _repeaters: [Repeater] = []
@@ -64,8 +79,9 @@ public final class QLAnchor<Input>: AnyAnchor {
6479
if let err = getReroutableError(newValue) {
6580
self.error = err
6681
} else {
82+
echo(value: newValue, timing: .early)
6783
dispatch(value: newValue)
68-
echo(value: newValue)
84+
echo(value: newValue, timing: .late)
6985
}
7086

7187
if (QLCommon.Config.Anchor.releaseValues) {
@@ -113,9 +129,10 @@ public final class QLAnchor<Input>: AnyAnchor {
113129
}
114130
}
115131

116-
private func echo(value: Input?) {
132+
private func echo(value: Input?, timing: Timing) {
117133
for repeater in _repeaters {
118-
repeater.echo(value: value, filter: echoFilter)
134+
guard repeater.timing == timing else { continue }
135+
repeater.echo(value: value, filter: echoFilter, timing: timing)
119136
}
120137
}
121138

Tests/QLoopTests/QLAnchorTests.swift

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ final class QLAnchorTests: XCTestCase {
8181
XCTAssert((receivedError as? QLCommon.Error) == QLCommon.Error.ThrownButNotSet)
8282
}
8383

84-
func test_given_it_has_repeaters_with_default_filter_when_input_set_then_it_echoes_to_them_as_well() {
84+
func test_given_it_has_early_repeaters_with_default_filter_when_input_set_then_it_echoes_to_them_as_well() {
8585
var receivedVal0: Int = -1
8686
var receivedVal1: Int = -1
8787
var receivedVal2: Int = -1
@@ -90,7 +90,30 @@ final class QLAnchorTests: XCTestCase {
9090
let expectRepeater2 = expectation(description: "should echo value to repeater2")
9191
let repeater1 = QLAnchor<Int>(onChange: { receivedVal1 = $0!; expectRepeater1.fulfill() })
9292
let repeater2 = QLAnchor<Int>(onChange: { receivedVal2 = $0!; expectRepeater2.fulfill() })
93-
let subject = QLAnchor<Int>(repeaters: repeater1, repeater2)
93+
let subject = QLAnchor<Int>(earlyRepeaters: repeater1, repeater2)
94+
XCTAssertEqual(subject.getRepeaters(timing: .early).count, 2)
95+
96+
subject.onChange = { receivedVal0 = $0!; expectOriginal0.fulfill() }
97+
98+
subject.value = 99
99+
100+
wait(for: [expectOriginal0, expectRepeater1, expectRepeater2], timeout: 8.0)
101+
XCTAssertEqual(receivedVal0, 99)
102+
XCTAssertEqual(receivedVal1, 99)
103+
XCTAssertEqual(receivedVal2, 99)
104+
}
105+
106+
func test_given_it_has_late_repeaters_with_default_filter_when_input_set_then_it_echoes_to_them_as_well() {
107+
var receivedVal0: Int = -1
108+
var receivedVal1: Int = -1
109+
var receivedVal2: Int = -1
110+
let expectOriginal0 = expectation(description: "should dispatch value")
111+
let expectRepeater1 = expectation(description: "should echo value to repeater1")
112+
let expectRepeater2 = expectation(description: "should echo value to repeater2")
113+
let repeater1 = QLAnchor<Int>(onChange: { receivedVal1 = $0!; expectRepeater1.fulfill() })
114+
let repeater2 = QLAnchor<Int>(onChange: { receivedVal2 = $0!; expectRepeater2.fulfill() })
115+
let subject = QLAnchor<Int>(lateRepeaters: repeater1, repeater2)
116+
XCTAssertEqual(subject.getRepeaters(timing: .late).count, 2)
94117

95118
subject.onChange = { receivedVal0 = $0!; expectOriginal0.fulfill() }
96119

@@ -113,7 +136,9 @@ final class QLAnchorTests: XCTestCase {
113136
onError: { receivedErr1 = $0; expectRepeater1.fulfill() })
114137
let repeater2 = QLAnchor<Int>(onChange: { _ in },
115138
onError: { receivedErr2 = $0; expectRepeater2.fulfill() })
116-
let subject = QLAnchor<Int>(repeaters: repeater1, repeater2)
139+
let subject = QLAnchor<Int>(earlyRepeaters: [repeater1], lateRepeaters: [repeater2])
140+
XCTAssertEqual(subject.getRepeaters(timing: .early).count, 1)
141+
XCTAssertEqual(subject.getRepeaters(timing: .late).count, 1)
117142

118143
subject.onError = { receivedErr0 = $0; expectOriginal0.fulfill() }
119144

@@ -125,7 +150,34 @@ final class QLAnchorTests: XCTestCase {
125150
XCTAssertNotNil(receivedErr2)
126151
}
127152

128-
func test_given_it_has_repeaters_with_custom_filter_when_input_set_then_it_dispatches_then_echoes_to_them_conditionally() {
153+
func test_given_it_has_earlyRepeaters_with_custom_filter_when_input_set_then_it_dispatches_then_echoes_to_them_conditionally() {
154+
var receivedVal0: Int = -1
155+
var receivedVal1: Int = -1
156+
var receivedVal2: Int = -1
157+
let expectOriginal0 = expectation(description: "should dispatch value")
158+
let expectRepeater2 = expectation(description: "should echo value to repeater2")
159+
let repeater1 = QLAnchor<Int>(onChange: { receivedVal1 = $0! })
160+
let repeater2 = QLAnchor<Int>(onChange: { receivedVal2 = $0!; expectRepeater2.fulfill() })
161+
162+
let subject = QLAnchor<Int>(
163+
echoFilter: ({ val, repeater in
164+
return (val == 11 && repeater === repeater1)
165+
|| (val == 22 && repeater === repeater2)
166+
}),
167+
earlyRepeaters: repeater1, repeater2
168+
)
169+
170+
subject.onChange = { receivedVal0 = $0!; expectOriginal0.fulfill() }
171+
172+
subject.value = 22
173+
174+
wait(for: [expectOriginal0, expectRepeater2], timeout: 8.0)
175+
XCTAssertEqual(receivedVal0, 22)
176+
XCTAssertEqual(receivedVal1, -1)
177+
XCTAssertEqual(receivedVal2, 22)
178+
}
179+
180+
func test_given_it_has_lateRepeaters_with_custom_filter_when_input_set_then_it_dispatches_then_echoes_to_them_conditionally() {
129181
var receivedVal0: Int = -1
130182
var receivedVal1: Int = -1
131183
var receivedVal2: Int = -1
@@ -139,7 +191,7 @@ final class QLAnchorTests: XCTestCase {
139191
return (val == 11 && repeater === repeater1)
140192
|| (val == 22 && repeater === repeater2)
141193
}),
142-
repeaters: repeater1, repeater2
194+
lateRepeaters: repeater1, repeater2
143195
)
144196

145197
subject.onChange = { receivedVal0 = $0!; expectOriginal0.fulfill() }

docs/changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
## Change Log
66

77

8+
<br />
9+
10+
### 0.1.7
11+
12+
- `QLAnchor` repeater timing (early/late)
13+
814
<br />
915

1016
### 0.1.6

docs/reference/QLAnchor.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020

2121
- init(onChange: `(Input?)->()`, onError: `(Error)->()` )
2222

23-
- init(repeaters: `QLAnchor.Repeater`, `...` )
23+
- init(earlyRepeaters: `QLAnchor.Repeater`, `...` )
24+
25+
- init(lateRepeaters: `QLAnchor.Repeater`, `...` )
2426

2527

2628
<br />
@@ -75,9 +77,9 @@ Repeaters offer a way to fork multiple streams off of the main path.
7577
When an Anchor has repeaters applied, then it will `echo` any `value` and `error` changes
7678
to each of them.
7779

78-
By default, it forwards all changes to all repeaters. In order to make it conditional, we can
79-
set an `EchoFilter`, which gets called prior to forwarding to each repeater. Return `false`
80-
from the EchoFilter to block that repeater from receiving the change.
80+
By default, it forwards all changes to all repeaters. In order to make it conditional,
81+
include an `EchoFilter`. Return `false` from the EchoFilter to block that repeater
82+
from receiving the change.
8183

8284

8385
##### EchoFilter

0 commit comments

Comments
 (0)