portico/Sources/Example/AsyncDemo.swift

92 lines
3.5 KiB
Swift

import Portico
/// Demonstrates Swift concurrency in Portico: a `Task` launched from a button
/// handler, or from the view's own `.task` lifecycle modifier, performs work
/// off the main actor in stages, and each stage's result is written straight
/// into `@State` when the `await` resumes on the main actor.
///
/// The whole module is `@MainActor`-isolated (`.defaultIsolation(MainActor.self)`
/// in `Package.swift`), so `runAsyncWork()`'s body inherits main-actor
/// isolation; only the explicit `Task.detached` blocks leave the main thread.
struct AsyncDemoPage: View {
private static let stageCount = 5
@State private var status = "Idle"
@State private var progress = 0.0
@State private var total = 0
@State private var isRunning = false
var body: some View {
Clamp {
StatusPage {
VStack(spacing: 12) {
Label { status }
.title2()
ProgressBar()
.fraction { progress }
.showText(true)
.text { "\(Int(progress * 100))%" }
.preferredWidth(240)
Label { "Accumulated total: \(total)" }
.dimmed()
Button("Run Background Work") { Task { await runAsyncWork() } }
.pill()
.suggestedAction()
.sensitive { !isRunning }
}
.halign(.center)
.task { await runAsyncWork() } // scoped to mapped lifetime; cancelled on unmap
}
.title("Swift Concurrency")
.description("Background stages update these widgets as they complete")
.iconName("system-run-symbolic")
}
.hexpand(true)
.vexpand(true)
}
/// Runs the staged background job. Re-entrant calls are ignored so a
/// second click (or a carousel re-map) cannot interleave two runs.
/// Cooperatively cancellable: checks `Task.isCancelled` before each
/// stage, so unmapping the view (which cancels the `.task`-owned Task)
/// stops the loop at the next stage boundary instead of running to
/// completion.
private func runAsyncWork() async {
guard !isRunning else { return }
isRunning = true
defer { isRunning = false }
status = "Starting..."
progress = 0
total = 0
print("[AsyncDemo] started")
for stage in 1...Self.stageCount {
guard !Task.isCancelled else {
print("[AsyncDemo] cancelled at stage \(stage)")
return
}
// Off the main actor: this is where real network or CPU work goes.
let chunk = await Self.work(stage: stage)
// Resumed on the main actor - these three writes drive three
// separate live widgets from inside the background job's loop.
total += chunk
progress = Double(stage) / Double(Self.stageCount)
status = "Stage \(stage) of \(Self.stageCount)"
print("[AsyncDemo] stage \(stage) -> total \(total)")
}
status = "Done: \(total)"
print("[AsyncDemo] finished: \(total)")
}
/// One stage of genuinely off-main-actor work.
private static func work(stage: Int) async -> Int {
await Task.detached(priority: .userInitiated) {
try? await Task.sleep(for: .milliseconds(300))
return stage * 10
}.value
}
}