Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3a515d1
refactor: isolate the core Workflow runtime to @MainActor
blakemcanally Jul 23, 2026
4d1d8b4
refactor: isolate Workflow protocol requirements to @MainActor
blakemcanally Jul 23, 2026
a4206c7
feat: add internal AsyncMulticaster for async host observation
blakemcanally Jul 23, 2026
8d6c85a
fix: never yield the initial value from a finished AsyncMulticaster
blakemcanally Jul 23, 2026
de0c545
feat: add AsyncStream-based renderings/outputs to WorkflowHost
blakemcanally Jul 23, 2026
8e73699
feat: expose AsyncStream outputs on WorkflowHostingController
blakemcanally Jul 23, 2026
532cafe
fix: annotate sample workflows for @MainActor protocol requirements
blakemcanally Jul 23, 2026
d7d1113
fix: annotate partner-module TestingTests suites for @MainActor
blakemcanally Jul 23, 2026
5a5a122
feat: add structured async runSideEffect with cooperative cancellation
blakemcanally Jul 23, 2026
1669617
chore: adopt Swift 6 language mode for the core Workflow target
blakemcanally Jul 23, 2026
7d031fa
docs: doc-comment updates for Swift Concurrency core APIs
blakemcanally Jul 23, 2026
578e1e8
fix: address final review findings (ordering comment, mapOutput isola…
blakemcanally Jul 23, 2026
85bf6fc
fix: avoid isolated deinit optimizer crash in release builds
blakemcanally Jul 24, 2026
dc1f50e
fix: schedule deinit finalization instead of running it inline
blakemcanally Jul 24, 2026
9936a30
fix: defer owned-subtree teardown with deinit finalization
blakemcanally Jul 24, 2026
17b95b5
fix: keep host teardown inline; defer only node-level subtree teardown
blakemcanally Jul 24, 2026
7eb839c
Run deinit finalization inline when released from a main-actor task
blakemcanally Jul 24, 2026
6de8764
Base deinit finalization dispatch on runtime activity, not task context
blakemcanally Jul 24, 2026
f83d5cd
Drain deferred deinit finalizations at operation exit, not via Task
blakemcanally Jul 24, 2026
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
4 changes: 3 additions & 1 deletion .mise.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
[tools]
tuist = "4.23.0"
# 4.27.0+ is required to parse swift-tools-version 6.0 package manifests
# (older versions fail decoding the manifest's build-settings format).
tuist = "4.27.0"
swiftformat = "0.54.2"

[settings]
Expand Down
12 changes: 9 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version:6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import CompilerPluginSupport
Expand Down Expand Up @@ -209,7 +209,7 @@ let package = Package(
path: "ViewEnvironmentUI/Sources"
),
],
swiftLanguageVersions: [.v5]
swiftLanguageModes: [.v5]
)

// MARK: Helpers
Expand All @@ -222,8 +222,14 @@ extension PackageDescription.Product {
}
}

let swift6Targets: Set<String> = ["Workflow"]

for target in package.targets {
var settings = target.swiftSettings ?? []
settings.append(.enableExperimentalFeature("StrictConcurrency=targeted"))
if swift6Targets.contains(target.name) {
settings.append(.swiftLanguageMode(.v6))
} else {
settings.append(.enableExperimentalFeature("StrictConcurrency=targeted"))
}
target.swiftSettings = settings
}
1 change: 1 addition & 0 deletions Samples/AsyncWorker/Sources/AsyncWorkerWorkflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ extension AsyncWorkerWorkflow {
extension AsyncWorkerWorkflow {
typealias Rendering = MessageScreen

@MainActor
func render(state: AsyncWorkerWorkflow.State, context: RenderContext<AsyncWorkerWorkflow>) -> Rendering {
NetworkRequestWorker()
.mapOutput { result in
Expand Down
1 change: 1 addition & 0 deletions Samples/SampleApp/Sources/DemoWorkflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ struct RefreshWorker: Worker {
extension DemoWorkflow {
typealias Rendering = DemoScreen

@MainActor
func render(state: DemoWorkflow.State, context: RenderContext<DemoWorkflow>) -> Rendering {
let color: UIColor = switch state.colorState {
case .red:
Expand Down
1 change: 1 addition & 0 deletions Samples/SampleApp/Sources/RootWorkflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ extension RootWorkflow {
extension RootWorkflow {
typealias Rendering = CrossFadeScreen

@MainActor
func render(state: RootWorkflow.State, context: RenderContext<RootWorkflow>) -> Rendering {
switch state {
case .welcome:
Expand Down
1 change: 1 addition & 0 deletions Samples/SampleApp/Sources/WelcomeWorkflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ extension WelcomeWorkflow {
extension WelcomeWorkflow {
typealias Rendering = WelcomeScreen

@MainActor
func render(state: WelcomeWorkflow.State, context: RenderContext<WelcomeWorkflow>) -> Rendering {
let sink = context.makeSink(of: Action.self)
return WelcomeScreen(
Expand Down
2 changes: 2 additions & 0 deletions Samples/SplitScreenContainer/DemoApp/DemoWorkflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ extension DemoWorkflow {
private static let colors: [UIColor] = [.red, .blue, .green, .yellow]
private static let complimentaryColors: [UIColor] = [.blue, .green, .yellow, .purple]

@MainActor
func render(state: State, context: RenderContext<DemoWorkflow>) -> Rendering {
let sink = context.makeSink(of: Action.self)

Expand All @@ -75,6 +76,7 @@ extension DemoWorkflow {
)
}

@MainActor
private func leadingScreenFor(state: State, context: RenderContext<DemoWorkflow>) -> AnyScreen {
let sink = context.makeSink(of: Action.self)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ extension AuthenticationWorkflow {
extension AuthenticationWorkflow {
typealias Rendering = AlertContainerScreen<ModalContainerScreen<BackStackScreen<AnyScreen>>>

@MainActor
func render(state: AuthenticationWorkflow.State, context: RenderContext<AuthenticationWorkflow>) -> Rendering {
let sink = context.makeSink(of: Action.self)

Expand Down Expand Up @@ -235,6 +236,7 @@ extension AuthenticationWorkflow {
)
}

@MainActor
private func twoFactorScreen(error: AuthenticationService.AuthenticationError?, intermediateSession: String, sink: Sink<Action>) -> BackStackScreen<AnyScreen>.Item {
let title: String = if let authenticationError = error {
authenticationError.localizedDescription
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ extension LoginWorkflow {
extension LoginWorkflow {
typealias Rendering = LoginScreen

@MainActor
func render(state: LoginWorkflow.State, context: RenderContext<LoginWorkflow>) -> Rendering {
let sink = context.makeSink(of: Action.self)

Expand Down
1 change: 1 addition & 0 deletions Samples/TicTacToe/Sources/Game/ConfirmQuitWorkflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ extension ConfirmQuitWorkflow {
extension ConfirmQuitWorkflow {
typealias Rendering = (ConfirmQuitScreen, Alert?)

@MainActor
func render(state: ConfirmQuitWorkflow.State, context: RenderContext<ConfirmQuitWorkflow>) -> Rendering {
let sink = context.makeSink(of: Action.self)
var alert: Alert?
Expand Down
2 changes: 2 additions & 0 deletions Samples/TicTacToe/Sources/Game/RunGameWorkflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ extension RunGameWorkflow {
extension RunGameWorkflow {
typealias Rendering = AlertContainerScreen<ModalContainerScreen<BackStackScreen<AnyScreen>>>

@MainActor
func render(state: RunGameWorkflow.State, context: RenderContext<RunGameWorkflow>) -> Rendering {
let sink = context.makeSink(of: Action.self)
var modals: [ModalContainerScreenModal] = []
Expand Down Expand Up @@ -160,6 +161,7 @@ extension RunGameWorkflow {
return AlertContainerScreen(baseScreen: modalContainerScreen, alert: alert)
}

@MainActor
private func newGameScreen(sink: Sink<Action>, playerX: String, playerO: String) -> NewGameScreen {
NewGameScreen(
playerX: playerX,
Expand Down
1 change: 1 addition & 0 deletions Samples/TicTacToe/Sources/Game/TakeTurnsWorkflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ extension TakeTurnsWorkflow {
extension TakeTurnsWorkflow {
typealias Rendering = GamePlayScreen

@MainActor
func render(state: TakeTurnsWorkflow.State, context: RenderContext<TakeTurnsWorkflow>) -> Rendering {
let sink = context.makeSink(of: Action.self)

Expand Down
1 change: 1 addition & 0 deletions Samples/TicTacToe/Sources/Main/MainWorkflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ extension MainWorkflow {
extension MainWorkflow {
typealias Rendering = AlertContainerScreen<ModalContainerScreen<BackStackScreen<AnyScreen>>>

@MainActor
func render(state: MainWorkflow.State, context: RenderContext<MainWorkflow>) -> Rendering {
switch state {
case .authenticating:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import XCTest

@testable import TicTacToe

@MainActor
class AuthenticationWorkflowTests: XCTestCase {
// MARK: Action Tests

Expand Down
1 change: 1 addition & 0 deletions Samples/TicTacToe/Tests/ConfirmQuitWorkflowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import XCTest

@testable import TicTacToe

@MainActor
class ConfirmQuitWorkflowTests: XCTestCase {
// MARK: Action Tests

Expand Down
1 change: 1 addition & 0 deletions Samples/TicTacToe/Tests/LoginWorkflowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import XCTest

@testable import TicTacToe

@MainActor
class LoginWorkflowTests: XCTestCase {
// MARK: Action Tests

Expand Down
1 change: 1 addition & 0 deletions Samples/TicTacToe/Tests/MainWorkflowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import XCTest

@testable import TicTacToe

@MainActor
class MainWorkflowTests: XCTestCase {
// MARK: Action Tests

Expand Down
1 change: 1 addition & 0 deletions Samples/TicTacToe/Tests/RunGameWorkflowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import XCTest

@testable import TicTacToe

@MainActor
class RunGameWorkflowTests: XCTestCase {
// MARK: Action Tests

Expand Down
1 change: 1 addition & 0 deletions Samples/TicTacToe/Tests/TakeTurnsWorkflowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import XCTest

@testable import TicTacToe

@MainActor
class TakeTurnsWorkflowTests: XCTestCase {
// MARK: Action Tests

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ extension WelcomeWorkflow {
extension WelcomeWorkflow {
typealias Rendering = WelcomeScreen

@MainActor
func render(state: WelcomeWorkflow.State, context: RenderContext<WelcomeWorkflow>) -> Rendering {
// Create a "sink" of type `Action`. A sink is what we use to send actions to the workflow.
let sink = context.makeSink(of: Action.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ extension RootWorkflow {
extension RootWorkflow {
typealias Rendering = BackStackScreen<AnyScreen>

@MainActor
func render(state: RootWorkflow.State, context: RenderContext<RootWorkflow>) -> Rendering {
// Create a sink to handle the back action from the TodoListWorkflow to log out.
let sink = context.makeSink(of: Action.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ extension WelcomeWorkflow {
extension WelcomeWorkflow {
typealias Rendering = WelcomeScreen

@MainActor
func render(state: WelcomeWorkflow.State, context: RenderContext<WelcomeWorkflow>) -> Rendering {
// Create a "sink" of type `Action`. A sink is what we use to send actions to the workflow.
let sink = context.makeSink(of: Action.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ extension RootWorkflow {
extension RootWorkflow {
typealias Rendering = BackStackScreen<AnyScreen>

@MainActor
func render(state: RootWorkflow.State, context: RenderContext<RootWorkflow>) -> Rendering {
// Delete the `let sink = context.makeSink(of: ...) as we no longer need a sink.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ extension TodoEditWorkflow {
extension TodoEditWorkflow {
typealias Rendering = BackStackScreen<AnyScreen>.Item

@MainActor
func render(state: TodoEditWorkflow.State, context: RenderContext<TodoEditWorkflow>) -> Rendering {
// The sink is used to send actions back to this workflow.
let sink = context.makeSink(of: Action.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ extension TodoListWorkflow {
extension TodoListWorkflow {
typealias Rendering = [BackStackScreen<AnyScreen>.Item]

@MainActor
func render(state: TodoListWorkflow.State, context: RenderContext<TodoListWorkflow>) -> Rendering {
// Define a sink to be able to send actions.
let sink = context.makeSink(of: Action.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ extension WelcomeWorkflow {
extension WelcomeWorkflow {
typealias Rendering = WelcomeScreen

@MainActor
func render(state: WelcomeWorkflow.State, context: RenderContext<WelcomeWorkflow>) -> Rendering {
// Create a "sink" of type `Action`. A sink is what we use to send actions to the workflow.
let sink = context.makeSink(of: Action.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ extension RootWorkflow {
extension RootWorkflow {
typealias Rendering = BackStackScreen<AnyScreen>

@MainActor
func render(state: RootWorkflow.State, context: RenderContext<RootWorkflow>) -> Rendering {
// Our list of back stack items. Will always include the "WelcomeScreen".
var backStackItems: [BackStackScreen<AnyScreen>.Item] = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ extension TodoEditWorkflow {
extension TodoEditWorkflow {
typealias Rendering = BackStackScreen<AnyScreen>.Item

@MainActor
func render(state: TodoEditWorkflow.State, context: RenderContext<TodoEditWorkflow>) -> Rendering {
// The sink is used to send actions back to this workflow.
let sink = context.makeSink(of: Action.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ extension TodoListWorkflow {
extension TodoListWorkflow {
typealias Rendering = BackStackScreen<AnyScreen>.Item

@MainActor
func render(state: TodoListWorkflow.State, context: RenderContext<TodoListWorkflow>) -> Rendering {
// Define a sink to be able to send actions.
let sink = context.makeSink(of: Action.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ extension TodoWorkflow {
extension TodoWorkflow {
typealias Rendering = [BackStackScreen<AnyScreen>.Item]

@MainActor
func render(state: TodoWorkflow.State, context: RenderContext<TodoWorkflow>) -> Rendering {
let todoListItem = TodoListWorkflow(name: name, todos: state.todos)
.mapOutput { output -> ListAction in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ extension WelcomeWorkflow {
extension WelcomeWorkflow {
typealias Rendering = WelcomeScreen

@MainActor
func render(state: WelcomeWorkflow.State, context: RenderContext<WelcomeWorkflow>) -> Rendering {
// Create a "sink" of type `Action`. A sink is what we use to send actions to the workflow.
let sink = context.makeSink(of: Action.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ extension RootWorkflow {
extension RootWorkflow {
typealias Rendering = BackStackScreen<AnyScreen>

@MainActor
func render(state: RootWorkflow.State, context: RenderContext<RootWorkflow>) -> Rendering {
// Our list of back stack items. Will always include the "WelcomeScreen".
var backStackItems: [BackStackScreen<AnyScreen>.Item] = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ extension TodoEditWorkflow {
extension TodoEditWorkflow {
typealias Rendering = BackStackScreen<AnyScreen>.Item

@MainActor
func render(state: TodoEditWorkflow.State, context: RenderContext<TodoEditWorkflow>) -> Rendering {
// The sink is used to send actions back to this workflow.
let sink = context.makeSink(of: Action.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ extension TodoListWorkflow {
extension TodoListWorkflow {
typealias Rendering = BackStackScreen<AnyScreen>.Item

@MainActor
func render(state: TodoListWorkflow.State, context: RenderContext<TodoListWorkflow>) -> Rendering {
// Define a sink to be able to send actions.
let sink = context.makeSink(of: Action.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ extension TodoWorkflow {
extension TodoWorkflow {
typealias Rendering = [BackStackScreen<AnyScreen>.Item]

@MainActor
func render(state: TodoWorkflow.State, context: RenderContext<TodoWorkflow>) -> Rendering {
let todoListItem = TodoListWorkflow(name: name, todos: state.todos)
.mapOutput { output -> ListAction in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ extension WelcomeWorkflow {
extension WelcomeWorkflow {
typealias Rendering = WelcomeScreen

@MainActor
func render(state: WelcomeWorkflow.State, context: RenderContext<WelcomeWorkflow>) -> Rendering {
// Create a "sink" of type `Action`. A sink is what we use to send actions to the workflow.
let sink = context.makeSink(of: Action.self)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import XCTest
// Import `WorkflowUI` as testable so that the wrappedScreen in `AnyScreen` can be accessed.
@testable import WorkflowUI

@MainActor
class RootWorkflowTests: XCTestCase {
func testWelcomeRendering() throws {
RootWorkflow()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import WorkflowTesting
import XCTest
@testable import Tutorial5Complete

@MainActor
class TodoEditWorkflowTests: XCTestCase {
func testAction() throws {
TodoEditWorkflow.Action
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import WorkflowTesting
import XCTest
@testable import Tutorial5Complete

@MainActor
class TodoListWorkflowTests: XCTestCase {
func testActions() throws {
TodoListWorkflow.Action
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import WorkflowUI
import XCTest
@testable import Tutorial5Complete

@MainActor
class TodoWorkflowTests: XCTestCase {
func testSelectingTodo() throws {
let todos: [TodoModel] = [TodoModel(title: "Title", note: "Note")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import WorkflowTesting
import XCTest
@testable import Tutorial5Complete

@MainActor
class WelcomeWorkflowTests: XCTestCase {
func testNameUpdates() throws {
WelcomeWorkflow.Action
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ extension DemoWorkflow {
extension DemoWorkflow {
typealias Rendering = DemoScreen

@MainActor
func render(state: DemoWorkflow.State, context: RenderContext<DemoWorkflow>) -> Rendering {
// Combine-based worker example
DemoWorker()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import WorkflowTesting
import XCTest
@testable import WorkflowCombineSampleApp

@MainActor
class DemoWorkflowTests: XCTestCase {
func test_demoWorkflow_publishesNewDate() {
let expectedDate = Date(timeIntervalSince1970: 0)
Expand Down
Loading
Loading