From ea884f7f3c0ae4df040274b6b26e6749d3cd7891 Mon Sep 17 00:00:00 2001 From: johnnewman-square Date: Fri, 26 Sep 2025 16:32:10 -0400 Subject: [PATCH 1/3] =?UTF-8?q?Adding=20a=20way=20for=20a=20WorkflowHost?= =?UTF-8?q?=E2=80=99s=20rendering=20to=20be=20managed.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Workflow/Sources/WorkflowHost.swift | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/Workflow/Sources/WorkflowHost.swift b/Workflow/Sources/WorkflowHost.swift index 3dcc43689..0e06752d4 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 + /// its 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:)` when `managedRenderings` is `true`, and + /// temporarily allows the `rootNode` to be rendered. + @_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 } } From edbab032de6c6e65bc8ac74151024b696b0b8f18 Mon Sep 17 00:00:00 2001 From: johnnewman-square Date: Mon, 29 Sep 2025 16:46:41 -0400 Subject: [PATCH 2/3] Adding a test for managed renderings. --- Workflow/Tests/WorkflowHostTests.swift | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Workflow/Tests/WorkflowHostTests.swift b/Workflow/Tests/WorkflowHostTests.swift index 12b9907b9..6a42291f8 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,28 @@ 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 traditional render pass not rendering the underlying + // workflow when its renderings are managed. This render pass may also + // come from an action applied to the workflow. + host.update(workflow: TestWorkflow(step: .second)) + XCTAssertEqual(host.rendering.value, 1) + + // A managed update will render the workflow. + host.managedUpdate(workflow: TestWorkflow(step: .second)) + XCTAssertEqual(host.rendering.value, 2) + } +} + // MARK: Runtime Configuration extension WorkflowHostTests { From b941c5ad5a2ab653b1b87bed1fdb21ec238397ab Mon Sep 17 00:00:00 2001 From: johnnewman-square Date: Tue, 30 Sep 2025 16:28:46 -0400 Subject: [PATCH 3/3] Documentation and unit test updates. --- Workflow/Sources/WorkflowHost.swift | 6 +++--- Workflow/Tests/WorkflowHostTests.swift | 23 ++++++++++++++++++++--- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/Workflow/Sources/WorkflowHost.swift b/Workflow/Sources/WorkflowHost.swift index 0e06752d4..672822eb7 100644 --- a/Workflow/Sources/WorkflowHost.swift +++ b/Workflow/Sources/WorkflowHost.swift @@ -94,14 +94,14 @@ public final class WorkflowHost { /// /// This property can be used when fine-grained control over the rendering /// of a `WorkflowHost` is needed, like cases where a container maintains - /// its hosts and propagates their output & renderings to the main Workflow + /// 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:)` when `managedRenderings` is `true`, and - /// temporarily allows the `rootNode` to be rendered. + /// Executes `update(workflow:)`, temporarily allowing the `rootNode` to be + /// rendered when `managedRenderings` is `true`. @_spi(WorkflowHostManagement) public func managedUpdate(workflow: WorkflowType) { let previousValue = managedRenderings diff --git a/Workflow/Tests/WorkflowHostTests.swift b/Workflow/Tests/WorkflowHostTests.swift index 6a42291f8..85e2ed7a0 100644 --- a/Workflow/Tests/WorkflowHostTests.swift +++ b/Workflow/Tests/WorkflowHostTests.swift @@ -98,15 +98,32 @@ extension WorkflowHostTests { host.managedRenderings = true XCTAssertEqual(host.rendering.value, 1) - // Example of a traditional render pass not rendering the underlying - // workflow when its renderings are managed. This render pass may also + // 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 render the workflow. + // 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) } }