Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ let package = Package(
name: "Workflow",
dependencies: [
.product(name: "IssueReporting", package: "xctest-dynamic-overlay"),
.product(name: "ReactiveSwift", package: "ReactiveSwift"),
],
path: "Workflow/Sources"
),

.target(
name: "WorkflowTesting",
dependencies: [
Expand Down
5 changes: 4 additions & 1 deletion Samples/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ let project = Project(
.unitTest(
for: "Workflow",
sources: "../Workflow/Tests/**",
dependencies: [.external(name: "Workflow")]
dependencies: [
.external(name: "ReactiveSwift"),
.external(name: "Workflow"),
]
),
.unitTest(
for: "WorkflowTesting",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class RootWorkflowTests: XCTestCase {

// First rendering is just the welcome screen. Update the name.
do {
let backStack = workflowHost.rendering.value
let backStack = workflowHost.rendering
XCTAssertEqual(1, backStack.items.count)

guard let welcomeScreen = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else {
Expand All @@ -100,7 +100,7 @@ class RootWorkflowTests: XCTestCase {

// Log in and go to the todo list.
do {
let backStack = workflowHost.rendering.value
let backStack = workflowHost.rendering
XCTAssertEqual(1, backStack.items.count)

guard let welcomeScreen = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else {
Expand All @@ -113,7 +113,7 @@ class RootWorkflowTests: XCTestCase {

// Expect the todo list to be rendered. Edit the first todo.
do {
let backStack = workflowHost.rendering.value
let backStack = workflowHost.rendering
XCTAssertEqual(2, backStack.items.count)

guard let _ = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else {
Expand All @@ -134,7 +134,7 @@ class RootWorkflowTests: XCTestCase {

// Selected a todo to edit. Expect the todo edit screen.
do {
let backStack = workflowHost.rendering.value
let backStack = workflowHost.rendering
XCTAssertEqual(3, backStack.items.count)

guard let _ = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else {
Expand All @@ -158,7 +158,7 @@ class RootWorkflowTests: XCTestCase {

// Save the selected todo.
do {
let backStack = workflowHost.rendering.value
let backStack = workflowHost.rendering
XCTAssertEqual(3, backStack.items.count)

guard let _ = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else {
Expand Down Expand Up @@ -204,7 +204,7 @@ class RootWorkflowTests: XCTestCase {

// Expect the todo list. Validate the title was updated.
do {
let backStack = workflowHost.rendering.value
let backStack = workflowHost.rendering
XCTAssertEqual(2, backStack.items.count)

guard let _ = backStack.items[0].screen.wrappedScreen as? WelcomeScreen else {
Expand Down
39 changes: 26 additions & 13 deletions Workflow/Sources/WorkflowHost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
* limitations under the License.
*/

import Combine
import Dispatch
import ReactiveSwift

/// Defines a type that receives debug information about a running workflow hierarchy.
public protocol WorkflowDebugger {
Expand All @@ -31,18 +31,31 @@ public protocol WorkflowDebugger {
func didUpdate(snapshot: WorkflowHierarchyDebugSnapshot, updateInfo: WorkflowUpdateDebugInfo)
}

/// Manages an active workflow hierarchy.
public final class WorkflowHost<WorkflowType: Workflow> {
private let (outputEvent, outputEventObserver) = Signal<WorkflowType.Output, Never>.pipe()
/// This protocol provides a way for an output extension to be added to both WorkflowHost and WorkflowHostingController in WorkflowReactiveSwift.
public protocol _WorkflowOutputPublisher {
associatedtype Output

var outputPublisher: AnyPublisher<Output, Never> { get }
}

/// Manages an active workflow hierarchy.
public final class WorkflowHost<WorkflowType: Workflow>: _WorkflowOutputPublisher {
// @testable
let rootNode: WorkflowNode<WorkflowType>

private let mutableRendering: MutableProperty<WorkflowType.Rendering>
private let renderingSubject: CurrentValueSubject<WorkflowType.Rendering, Never>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slightly inclined to split the storage for the property vs the observer, since that will give us the most control over when the 'outside world' sees the update. e.g.

var rendering: Rendering
let renderingSubject = PassThroughSubject<Rendering, Never>()

any thoughts on that? i guess one thing that would be different is that the old way (and presumably using a CVS) would have some baked-in synchronization mechanism over the underlying value. in theory this stuff should be main-thread-only though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We currently are using a ReactiveSwift MutableProperty which does have an internal lock. Yes CurrentValueSubject does have some baked in serialization mechanism albeit without Apple documentation on what that is. Since we can't enforce someone reading the rendering value on the main thread I'm inclined to use the CurrentValueSubject since it's about as close as we can get as a Combine version of what we have now.
If we split the storage for the property out we could always lock around getting/setting it so I'm not against doing that but I think CurrentValueSubject should work for how we are using it.

private let outputSubject = PassthroughSubject<WorkflowType.Output, Never>()

/// Represents the `Rendering` produced by the root workflow in the hierarchy. New `Rendering` values are produced
/// as state transitions occur within the hierarchy.
public let rendering: Property<WorkflowType.Rendering>
public var rendering: WorkflowType.Rendering {
renderingSubject.value
}

/// A Publisher containing rendering events produced by the root workflow in the hierarchy.
public var renderingPublisher: AnyPublisher<WorkflowType.Rendering, Never> {
renderingSubject.eraseToAnyPublisher()
}

/// Context object to pass down to descendant nodes in the tree.
let context: HostContext
Expand Down Expand Up @@ -88,8 +101,8 @@ public final class WorkflowHost<WorkflowType: Workflow> {
parentSession: nil
)

self.mutableRendering = MutableProperty(rootNode.render())
self.rendering = Property(mutableRendering)
self.renderingSubject = CurrentValueSubject(rootNode.render())

rootNode.enableEvents()

debugger?.didEnterInitialState(snapshot: rootNode.makeDebugSnapshot())
Expand Down Expand Up @@ -130,12 +143,12 @@ public final class WorkflowHost<WorkflowType: Workflow> {
private func handle(output: WorkflowNode<WorkflowType>.Output) {
let shouldRender = !shouldSkipRenderForOutput(output)
if shouldRender {
mutableRendering.value = rootNode.render()
renderingSubject.send(rootNode.render())
}

// Always emit an output, regardless of whether a render occurs
if let outputEvent = output.outputEvent {
outputEventObserver.send(value: outputEvent)
outputSubject.send(outputEvent)
}

debugger?.didUpdate(
Expand All @@ -149,9 +162,9 @@ public final class WorkflowHost<WorkflowType: Workflow> {
}
}

/// A signal containing output events emitted by the root workflow in the hierarchy.
public var output: Signal<WorkflowType.Output, Never> {
outputEvent
/// A publisher containing output events emitted by the root workflow in the hierarchy.
public var outputPublisher: AnyPublisher<WorkflowType.Output, Never> {
outputSubject.eraseToAnyPublisher()
}
}

Expand Down
8 changes: 5 additions & 3 deletions Workflow/Tests/AnyWorkflowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import ReactiveSwift
import Combine
import XCTest
@testable import Workflow

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

let renderingExpectation = expectation(description: "Waiting for rendering")
host.rendering.producer.startWithValues { rendering in
let cancellable = host.renderingPublisher.sink { rendering in
if rendering {
renderingExpectation.fulfill()
}
}

let outputExpectation = expectation(description: "Waiting for output")
host.output.observeValues { output in
let outputCancellable = host.outputPublisher.sink { output in
if output {
outputExpectation.fulfill()
}
}
wait(for: [renderingExpectation, outputExpectation], timeout: 1)
cancellable.cancel()
outputCancellable.cancel()
}

func testOnlyWrapsOnce() {
Expand Down
Loading
Loading