diff --git a/Workflow/Sources/WorkflowHost.swift b/Workflow/Sources/WorkflowHost.swift index 3dcc43689..672822eb7 100644 --- a/Workflow/Sources/WorkflowHost.swift +++ b/Workflow/Sources/WorkflowHost.swift @@ -89,6 +89,27 @@ public final class WorkflowHost { } } + /// When `true`, the host will not render its `rootNode` unless using the + /// `managedUpdate(workflow:)` function. + /// + /// This property can be used when fine-grained control over the rendering + /// of a `WorkflowHost` is needed, like cases where a container maintains + /// hosts and propagates their output & renderings to the main Workflow + /// tree. Without this mechanism, detached hosts may render extra times: + /// once for the applied action and again when the main tree renders. + @_spi(WorkflowHostManagement) + public var managedRenderings: Bool = false + + /// Executes `update(workflow:)`, temporarily allowing the `rootNode` to be + /// rendered when `managedRenderings` is `true`. + @_spi(WorkflowHostManagement) + public func managedUpdate(workflow: WorkflowType) { + let previousValue = managedRenderings + managedRenderings = false + update(workflow: workflow) + managedRenderings = previousValue + } + /// Update the input for the workflow. Will cause a render pass. public func update(workflow: WorkflowType) { rootNode.update(workflow: workflow) @@ -144,8 +165,9 @@ extension WorkflowHost { // We can skip the render pass if: // 1. The runtime config supports this behavior. // 2. No subtree invalidation occurred during action processing. - context.runtimeConfig.renderOnlyIfStateChanged - && !output.subtreeInvalidated + // 3. Alternatively, if the host's rendering is managed externally. + (context.runtimeConfig.renderOnlyIfStateChanged + && !output.subtreeInvalidated) || managedRenderings } } diff --git a/Workflow/Tests/WorkflowHostTests.swift b/Workflow/Tests/WorkflowHostTests.swift index 12b9907b9..85e2ed7a0 100644 --- a/Workflow/Tests/WorkflowHostTests.swift +++ b/Workflow/Tests/WorkflowHostTests.swift @@ -16,6 +16,7 @@ import XCTest @_spi(WorkflowRuntimeConfig) @testable import Workflow +@_spi(WorkflowHostManagement) import Workflow final class WorkflowHostTests: XCTestCase { func test_updatedInputCausesRenderPass() { @@ -87,6 +88,45 @@ final class WorkflowHost_EventEmissionTests: XCTestCase { } } +// MARK: Host Management + +extension WorkflowHostTests { + func test_managed_renderings() { + let host = WorkflowHost( + workflow: TestWorkflow(step: .first) + ) + host.managedRenderings = true + XCTAssertEqual(host.rendering.value, 1) + + // Example of a standard render pass. This will not render the hosted + // workflow when renderings are managed. This render pass could also + // come from an action applied to the workflow. + host.update(workflow: TestWorkflow(step: .second)) + XCTAssertEqual(host.rendering.value, 1) + + // A managed update will always render the workflow. + host.managedUpdate(workflow: TestWorkflow(step: .second)) + XCTAssertEqual(host.rendering.value, 2) + + // Ensure that the flag is still enabled. + XCTAssertTrue(host.managedRenderings) + } + + func test_managed_renderings_when_not_set() { + let host = WorkflowHost( + workflow: TestWorkflow(step: .first) + ) + XCTAssertEqual(host.rendering.value, 1) + + // A managed update will always render the workflow. + host.managedUpdate(workflow: TestWorkflow(step: .second)) + XCTAssertEqual(host.rendering.value, 2) + + // Ensure that the flag wasn't inadvertently enabled. + XCTAssertFalse(host.managedRenderings) + } +} + // MARK: Runtime Configuration extension WorkflowHostTests {