Skip to content

Commit 59bcc8b

Browse files
committed
Remove ReactiveSwift from Workflow public interface
Added WorkflowOutputPublisher protocol Changed WorkflowHost and WorkflowHostingController to conform to WorkflowOutputPublisher. Added extension in WorkflowReactiveSwift to WorkflowOutputPublisher for output signal Fix added tests
1 parent 392de87 commit 59bcc8b

21 files changed

+203
-144
lines changed

Package.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,11 @@ let package = Package(
6868

6969
.target(
7070
name: "Workflow",
71-
dependencies: ["ReactiveSwift"],
7271
path: "Workflow/Sources"
7372
),
7473
.testTarget(
7574
name: "WorkflowTests",
76-
dependencies: ["Workflow"],
75+
dependencies: ["ReactiveSwift", "Workflow"],
7776
path: "Workflow/Tests"
7877
),
7978
.target(

Samples/Project.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,10 @@ let project = Project(
159159
.unitTest(
160160
for: "Workflow",
161161
sources: "../Workflow/Tests/**",
162-
dependencies: [.external(name: "Workflow")]
162+
dependencies: [
163+
.external(name: "ReactiveSwift"),
164+
.external(name: "Workflow"),
165+
]
163166
),
164167
.unitTest(
165168
for: "WorkflowTesting",
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import Combine
2+
3+
public struct ReadOnlyCurrentValueSubject<Output, Failure>: Combine.Publisher where Failure: Error {
4+
private let currentValueSubject: CurrentValueSubject<Output, Failure>
5+
6+
public var value: Output {
7+
currentValueSubject.value
8+
}
9+
10+
private init(_ value: Output) {
11+
self.currentValueSubject = CurrentValueSubject<Output, Failure>(value)
12+
}
13+
14+
public static func publisher(value: Output) -> (Self, CurrentValueSubject<Output, Failure>) {
15+
let publisher = Self(value)
16+
return (publisher, publisher.currentValueSubject)
17+
}
18+
19+
public func receive<S>(subscriber: S) where S: Subscriber, Failure == S.Failure, Output == S.Input {
20+
currentValueSubject.receive(subscriber: subscriber)
21+
}
22+
}

Workflow/Sources/WorkflowHost.swift

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import ReactiveSwift
17+
import Combine
1818

1919
/// Defines a type that receives debug information about a running workflow hierarchy.
2020
public protocol WorkflowDebugger {
@@ -30,18 +30,23 @@ public protocol WorkflowDebugger {
3030
func didUpdate(snapshot: WorkflowHierarchyDebugSnapshot, updateInfo: WorkflowUpdateDebugInfo)
3131
}
3232

33-
/// Manages an active workflow hierarchy.
34-
public final class WorkflowHost<WorkflowType: Workflow> {
35-
private let (outputEvent, outputEventObserver) = Signal<WorkflowType.Output, Never>.pipe()
33+
public protocol WorkflowOutputPublisher {
34+
associatedtype Output
35+
36+
var outputPublisher: AnyPublisher<Output, Never> { get }
37+
}
3638

39+
/// Manages an active workflow hierarchy.
40+
public final class WorkflowHost<WorkflowType: Workflow>: WorkflowOutputPublisher {
3741
// @testable
3842
let rootNode: WorkflowNode<WorkflowType>
3943

40-
private let mutableRendering: MutableProperty<WorkflowType.Rendering>
44+
private let mutableRendering: CurrentValueSubject<WorkflowType.Rendering, Never>
45+
private let outputSubject = PassthroughSubject<WorkflowType.Output, Never>()
4146

4247
/// Represents the `Rendering` produced by the root workflow in the hierarchy. New `Rendering` values are produced
4348
/// as state transitions occur within the hierarchy.
44-
public let rendering: Property<WorkflowType.Rendering>
49+
public let rendering: ReadOnlyCurrentValueSubject<WorkflowType.Rendering, Never>
4550

4651
/// Context object to pass down to descendant nodes in the tree.
4752
let context: HostContext
@@ -78,8 +83,8 @@ public final class WorkflowHost<WorkflowType: Workflow> {
7883
parentSession: nil
7984
)
8085

81-
self.mutableRendering = MutableProperty(rootNode.render())
82-
self.rendering = Property(mutableRendering)
86+
(self.rendering, self.mutableRendering) = ReadOnlyCurrentValueSubject.publisher(value: rootNode.render())
87+
8388
rootNode.enableEvents()
8489

8590
debugger?.didEnterInitialState(snapshot: rootNode.makeDebugSnapshot())
@@ -110,12 +115,12 @@ public final class WorkflowHost<WorkflowType: Workflow> {
110115
private func handle(output: WorkflowNode<WorkflowType>.Output) {
111116
let shouldRender = !shouldSkipRenderForOutput(output)
112117
if shouldRender {
113-
mutableRendering.value = rootNode.render()
118+
mutableRendering.send(rootNode.render())
114119
}
115120

116121
// Always emit an output, regardless of whether a render occurs
117122
if let outputEvent = output.outputEvent {
118-
outputEventObserver.send(value: outputEvent)
123+
outputSubject.send(outputEvent)
119124
}
120125

121126
debugger?.didUpdate(
@@ -129,9 +134,9 @@ public final class WorkflowHost<WorkflowType: Workflow> {
129134
}
130135
}
131136

132-
/// A signal containing output events emitted by the root workflow in the hierarchy.
133-
public var output: Signal<WorkflowType.Output, Never> {
134-
outputEvent
137+
/// A publisher containing output events emitted by the root workflow in the hierarchy.
138+
public var outputPublisher: AnyPublisher<WorkflowType.Output, Never> {
139+
outputSubject.eraseToAnyPublisher()
135140
}
136141
}
137142

Workflow/Tests/AnyWorkflowTests.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import ReactiveSwift
17+
import Combine
1818
import XCTest
1919
@testable import Workflow
2020

@@ -40,19 +40,21 @@ public class AnyWorkflowTests: XCTestCase {
4040
let host = WorkflowHost(workflow: OnOutputWorkflow())
4141

4242
let renderingExpectation = expectation(description: "Waiting for rendering")
43-
host.rendering.producer.startWithValues { rendering in
43+
let cancellable = host.rendering.sink { rendering in
4444
if rendering {
4545
renderingExpectation.fulfill()
4646
}
4747
}
4848

4949
let outputExpectation = expectation(description: "Waiting for output")
50-
host.output.observeValues { output in
50+
let outputCancellable = host.outputPublisher.sink { output in
5151
if output {
5252
outputExpectation.fulfill()
5353
}
5454
}
5555
wait(for: [renderingExpectation, outputExpectation], timeout: 1)
56+
cancellable.cancel()
57+
outputCancellable.cancel()
5658
}
5759

5860
func testOnlyWrapsOnce() {

Workflow/Tests/ConcurrencyTests.swift

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ final class ConcurrencyTests: XCTestCase {
2929
var first = true
3030
var observedScreen: TestScreen?
3131

32-
let disposable = host.rendering.signal.observeValues { rendering in
32+
let cancellable = host.rendering.sink { rendering in
3333
if first {
3434
expectation.fulfill()
3535
first = false
@@ -47,12 +47,10 @@ final class ConcurrencyTests: XCTestCase {
4747
wait(for: [expectation], timeout: 1.0)
4848
guard let screen = observedScreen else {
4949
XCTFail("Screen was not updated.")
50-
disposable?.dispose()
5150
return
5251
}
5352
XCTAssertEqual(1, screen.count)
54-
55-
disposable?.dispose()
53+
cancellable.cancel()
5654
}
5755

5856
// Events emitted between `render` on a workflow and `enableEvents` are queued and will be delivered asynchronously after rendering is updated.
@@ -62,7 +60,7 @@ final class ConcurrencyTests: XCTestCase {
6260
let renderingExpectation = expectation(description: "Waiting on rendering values.")
6361
var first = true
6462

65-
let disposable = host.rendering.signal.observeValues { rendering in
63+
let cancellable = host.rendering.dropFirst().sink { rendering in
6664
if first {
6765
first = false
6866
// Emit an event when the rendering is first received.
@@ -81,8 +79,7 @@ final class ConcurrencyTests: XCTestCase {
8179
waitForExpectations(timeout: 1)
8280

8381
XCTAssertEqual(2, host.rendering.value.count)
84-
85-
disposable?.dispose()
82+
cancellable.cancel()
8683
}
8784

8885
func test_multipleQueuedEvents() {
@@ -91,7 +88,7 @@ final class ConcurrencyTests: XCTestCase {
9188
let renderingExpectation = expectation(description: "Waiting on rendering values.")
9289
var renderingValuesCount = 0
9390

94-
let disposable = host.rendering.signal.observeValues { rendering in
91+
let cancellable = host.rendering.dropFirst().sink { rendering in
9592
if renderingValuesCount == 0 {
9693
// Emit two events.
9794
rendering.update()
@@ -116,8 +113,7 @@ final class ConcurrencyTests: XCTestCase {
116113
waitForExpectations(timeout: 1)
117114

118115
XCTAssertEqual(3, host.rendering.value.count)
119-
120-
disposable?.dispose()
116+
cancellable.cancel()
121117
}
122118

123119
// A `sink` is invalidated after a single action has been received. However, if the next `render` pass uses a sink
@@ -232,7 +228,7 @@ final class ConcurrencyTests: XCTestCase {
232228
var first = true
233229

234230
let renderingsComplete = expectation(description: "Waiting for renderings")
235-
let disposable = host.rendering.signal.observeValues { rendering in
231+
let cancellable = host.rendering.dropFirst().sink { rendering in
236232
if first {
237233
first = false
238234
rendering.update()
@@ -249,8 +245,7 @@ final class ConcurrencyTests: XCTestCase {
249245
XCTAssertEqual(2, debugger.snapshots.count)
250246
XCTAssertEqual("1", debugger.snapshots[0].stateDescription)
251247
XCTAssertEqual("2", debugger.snapshots[1].stateDescription)
252-
253-
disposable?.dispose()
248+
cancellable.cancel()
254249
}
255250

256251
func test_childWorkflowsAreSynchronous() {
@@ -318,11 +313,11 @@ final class ConcurrencyTests: XCTestCase {
318313

319314
let renderingExpectation = XCTestExpectation()
320315
let outputExpectation = XCTestExpectation()
321-
let outDisposable = host.output.signal.observeValues { output in
316+
let outputCancellable = host.outputPublisher.sink { output in
322317
outputExpectation.fulfill()
323318
}
324319

325-
let disposable = host.rendering.signal.observeValues { rendering in
320+
let cancellable = host.rendering.sink { rendering in
326321
renderingExpectation.fulfill()
327322
}
328323

@@ -337,8 +332,8 @@ final class ConcurrencyTests: XCTestCase {
337332

338333
XCTAssertEqual(101, host.rendering.value.count)
339334

340-
disposable?.dispose()
341-
outDisposable?.dispose()
335+
cancellable.cancel()
336+
outputCancellable.cancel()
342337
}
343338

344339
// Since event pipes are reused for the same type, validate that the `AnyWorkflowAction`

Workflow/Tests/RenderOnlyIfStateChangedTests.swift

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ final class RenderOnlyIfStateChangedEnabledTests: XCTestCase {
3333
let host = WorkflowHost(workflow: CounterWorkflow())
3434

3535
var renderCount = 0
36-
let disposable = host.rendering.signal.observeValues { _ in renderCount += 1 }
37-
defer { disposable?.dispose() }
36+
let cancellable = host.rendering.dropFirst().sink(receiveValue: { _ in renderCount += 1 })
37+
defer { cancellable.cancel() }
3838

3939
XCTAssertEqual(renderCount, 0)
4040
XCTAssertEqual(host.rendering.value.count, 0)
@@ -50,8 +50,8 @@ final class RenderOnlyIfStateChangedEnabledTests: XCTestCase {
5050
let host = WorkflowHost(workflow: CounterWorkflow())
5151

5252
var renderCount = 0
53-
let disposable = host.rendering.signal.observeValues { _ in renderCount += 1 }
54-
defer { disposable?.dispose() }
53+
let cancellable = host.rendering.dropFirst().sink(receiveValue: { _ in renderCount += 1 })
54+
defer { cancellable.cancel() }
5555

5656
XCTAssertEqual(renderCount, 0)
5757
XCTAssertEqual(host.rendering.value.count, 0)
@@ -67,8 +67,8 @@ final class RenderOnlyIfStateChangedEnabledTests: XCTestCase {
6767
let host = WorkflowHost(workflow: CounterWorkflow())
6868

6969
var renderCount = 0
70-
let disposable = host.rendering.signal.observeValues { _ in renderCount += 1 }
71-
defer { disposable?.dispose() }
70+
let cancellable = host.rendering.dropFirst().sink(receiveValue: { _ in renderCount += 1 })
71+
defer { cancellable.cancel() }
7272

7373
XCTAssertEqual(renderCount, 0)
7474
XCTAssertEqual(host.rendering.value.count, 0)
@@ -85,11 +85,11 @@ final class RenderOnlyIfStateChangedEnabledTests: XCTestCase {
8585
var renderCount = 0
8686
var outputs: [Int] = []
8787

88-
let renderDisposable = host.rendering.signal.observeValues { _ in renderCount += 1 }
89-
defer { renderDisposable?.dispose() }
88+
let cancellable = host.rendering.dropFirst().sink(receiveValue: { _ in renderCount += 1 })
89+
defer { cancellable.cancel() }
9090

91-
let outputDisposable = host.output.observeValues { outputs.append($0) }
92-
defer { outputDisposable?.dispose() }
91+
let outputCancellable = host.outputPublisher.sink(receiveValue: { outputs.append($0) })
92+
defer { outputCancellable.cancel() }
9393

9494
XCTAssertEqual(renderCount, 0)
9595
XCTAssertEqual(outputs, [])
@@ -104,8 +104,8 @@ final class RenderOnlyIfStateChangedEnabledTests: XCTestCase {
104104
let host = WorkflowHost(workflow: ParentWorkflow())
105105

106106
var renderCount = 0
107-
let disposable = host.rendering.signal.observeValues { _ in renderCount += 1 }
108-
defer { disposable?.dispose() }
107+
let cancellable = host.rendering.dropFirst().sink(receiveValue: { _ in renderCount += 1 })
108+
defer { cancellable.cancel() }
109109

110110
XCTAssertEqual(renderCount, 0)
111111

@@ -118,8 +118,8 @@ final class RenderOnlyIfStateChangedEnabledTests: XCTestCase {
118118
let host = WorkflowHost(workflow: CounterWorkflow())
119119

120120
var renderCount = 0
121-
let disposable = host.rendering.signal.observeValues { _ in renderCount += 1 }
122-
defer { disposable?.dispose() }
121+
let cancellable = host.rendering.dropFirst().sink(receiveValue: { _ in renderCount += 1 })
122+
defer { cancellable.cancel() }
123123

124124
XCTAssertEqual(renderCount, 0)
125125

@@ -134,11 +134,11 @@ final class RenderOnlyIfStateChangedEnabledTests: XCTestCase {
134134
var renderCount = 0
135135
var outputCount = 0
136136

137-
let renderDisposable = host.rendering.signal.observeValues { _ in renderCount += 1 }
138-
defer { renderDisposable?.dispose() }
137+
let cancellable = host.rendering.dropFirst().sink(receiveValue: { _ in renderCount += 1 })
138+
defer { cancellable.cancel() }
139139

140-
let outputDisposable = host.output.observeValues { _ in outputCount += 1 }
141-
defer { outputDisposable?.dispose() }
140+
let outputCancellable = host.outputPublisher.sink(receiveValue: { _ in outputCount += 1 })
141+
defer { outputCancellable.cancel() }
142142

143143
XCTAssertEqual(renderCount, 0)
144144
XCTAssertEqual(outputCount, 0)
@@ -156,12 +156,12 @@ final class RenderOnlyIfStateChangedEnabledTests: XCTestCase {
156156
var renderCount = 0
157157
var outputCount = 0
158158

159-
let renderDisposable = host.rendering.signal.observeValues { _ in renderCount += 1 }
160-
let outputDisposable = host.output.observeValues { _ in outputCount += 1 }
159+
let cancellable = host.rendering.dropFirst().sink(receiveValue: { _ in renderCount += 1 })
160+
let outputCancellable = host.outputPublisher.sink(receiveValue: { _ in outputCount += 1 })
161161

162162
defer {
163-
renderDisposable?.dispose()
164-
outputDisposable?.dispose()
163+
cancellable.cancel()
164+
outputCancellable.cancel()
165165
}
166166

167167
XCTAssertEqual(outputCount, 0)
@@ -185,12 +185,12 @@ final class RenderOnlyIfStateChangedEnabledTests: XCTestCase {
185185
var renderCount = 0
186186
var outputCount = 0
187187

188-
let renderDisposable = host.rendering.signal.observeValues { _ in renderCount += 1 }
189-
let outputDisposable = host.output.observeValues { _ in outputCount += 1 }
188+
let cancellable = host.rendering.dropFirst().sink(receiveValue: { _ in renderCount += 1 })
189+
let outputCancellable = host.outputPublisher.sink(receiveValue: { _ in outputCount += 1 })
190190

191191
defer {
192-
renderDisposable?.dispose()
193-
outputDisposable?.dispose()
192+
cancellable.cancel()
193+
outputCancellable.cancel()
194194
}
195195

196196
XCTAssertEqual(outputCount, 0)

0 commit comments

Comments
 (0)