92 lines
3.5 KiB
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
|
|
}
|
|
}
|