A Swift 6 library for managing concurrent async tasks by key, with configurable behavior for duplicate requests.
- Task Management in Swift Part 1: The Problem
- Task Management in Swift Part 2: Introducing the TaskStore
- Task Management in Swift Part 3: Duplicate Key Behavior
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/junebash/swift-task-store.git", from: "0.1.0")
]Note:
TaskStoremust be used from an isolated context (e.g.,@MainActoror a custom actor). The Swift 6.2 compiler enforces this requirement.
import TaskStore
@Observable
@MainActor
final class MyModel {
let tasks = TaskStore<TaskKey>()
enum TaskKey: Hashable {
case fetchUser
case saveDocument
}
func fetchUser() {
// Inherits MainActor isolation
tasks.addIsolatedTask(forKey: .fetchUser) {
try? await api.fetchUser()
}
}
func processInBackground() {
// Runs on global concurrent executor
tasks.addConcurrentTask(forKey: .processData) {
await heavyComputation()
}
}
var isFetching: Bool {
tasks.taskIsRunning(forKey: .fetchUser)
}
}When adding a task for a key that's already running:
| Behavior | Description |
|---|---|
.cancelPrevious(wait: false) |
Cancel previous, start new immediately (default) |
.cancelPrevious(wait: true) |
Cancel previous, wait for it, then start new |
.wait |
Wait for previous to complete, then start new |
.runConcurrently |
Run both tasks simultaneously |
.preferPrevious |
Keep existing task, ignore new request |
// Search: cancel previous searches immediately
tasks.addConcurrentTask(forKey: .search, duplicateKeyBehavior: .cancelPrevious(wait: false)) {
await performSearch(query)
}
// Save: wait for previous save to complete
tasks.addIsolatedTask(forKey: .save, duplicateKeyBehavior: .wait) {
await saveDocument()
}TaskStore supports optional task naming for debugging and instrumentation via the TaskNameProvider protocol:
// Use key description as task names
let store = TaskStore<TaskKey>(nameProvider: .keyDescription)
// Use a constant name
let store = TaskStore<TaskKey>(nameProvider: .constant("BackgroundTask"))
// Use a custom closure
let store = TaskStore<TaskKey>(nameProvider: .fromKey { key in
"Task-\(key)"
})
// Compose with modifiers
let store = TaskStore<TaskKey>(
nameProvider: .keyDescription
.withPrefix("MyViewModel")
.withIncrementingSuffix()
)
// Produces: "MyViewModel.fetchUser.0", "MyViewModel.fetchUser.1", etc.The name can be accessed from within the task via Task.name.
| Provider | Description |
|---|---|
.keyDescription |
Uses String(describing:) on the key |
.constant("name") |
Returns the same name for all keys |
.fromKey { ... } |
Uses a custom closure to generate names |
| Modifier | Description |
|---|---|
.withPrefix("prefix") |
Prepends a prefix (e.g., "App.fetchUser") |
.withIncrementingSuffix() |
Appends incrementing numbers (e.g., "fetchUser.0") |
// Add a task that runs on the global concurrent executor
@discardableResult
func addConcurrentTask(
forKey key: Key,
duplicateKeyBehavior: TaskStoreDuplicateKeyBehavior = .cancelPrevious(wait: false),
priority: TaskPriority? = nil,
operation: @escaping @Sendable () async -> Void
) -> Task<Void, Never>
// Add a task that inherits the caller's actor isolation
@discardableResult
func addIsolatedTask(
forKey key: Key,
duplicateKeyBehavior: TaskStoreDuplicateKeyBehavior = .cancelPrevious(wait: false),
priority: TaskPriority? = nil,
operation: @escaping () async -> Void
) -> Task<Void, Never>
// Add a task with immediate execution (Swift 6.2+, SE-0472)
// Runs synchronously until first suspension point, then continues asynchronously
@available(macOS 26.0, iOS 26.0, tvOS 26.0, watchOS 26.0, visionOS 26.0, *)
@discardableResult
func addImmediateTask(
forKey key: Key,
duplicateKeyBehavior: TaskStoreDuplicateKeyBehavior = .cancelPrevious(wait: false),
priority: TaskPriority? = nil,
operation: @escaping () async -> Void
) -> Task<Void, Never>
// Cancel a task
func cancelTask(forKey key: Key)
// Cancel all tasks
func cancelAllTasks()
// Check if a task is running
func taskIsRunning(forKey key: Key) -> Bool
// Get the current task for a key
func currentTask(forKey key: Key) -> Task<Void, Never>?
// Running task info
var runningTaskCount: Int
var runningTaskKeys: Set<Key>MIT