Elegant task orchestration for Swift apps - Control concurrent operations with precision
TaskManager is a powerful Swift library that brings order to chaos in asynchronous programming. While Swift's structured concurrency is excellent, unstructured tasks created with Task { }
run immediately and can lead to race conditions, redundant operations, and unpredictable behavior.
TaskManager solves this by providing:
- Task isolation by key - Group related operations together
- Execution control - Choose whether to cancel existing tasks or queue new ones
- SwiftUI integration - First-class support for UI-driven async operations
- Actor-based safety - Thread-safe by design using Swift actors
Add TaskManager to your Package.swift
:
dependencies: [
.package(url: "https://github.com/muukii/swift-concurrency-task-manager.git", from: "1.0.0")
]
Or add it through Xcode:
- File β Add Package Dependencies
- Enter the repository URL
- Click "Add Package"
- Swift 6.0+
- iOS 14.0+ / macOS 11.0+ / tvOS 16.0+ / watchOS 10.0+
- Xcode 15.0+
A TaskKey
is a unique identifier that groups related operations. Tasks with the same key are managed together, allowing you to control their execution behavior.
// Type-based keys for strong typing
enum UserOperations: TaskKeyType {}
let key = TaskKey(UserOperations.self)
// String-based keys for simplicity
let key: TaskKey = "user-fetch"
// Dynamic keys with combined values
let key = TaskKey(UserOperations.self).combined(userID)
// Unique keys for one-off operations
let key = TaskKey.distinct()
// Code location-based keys
let key = TaskKey.code() // Uses file:line:column
TaskManager offers two execution modes:
.dropCurrent
- Cancels any running task with the same key before starting the new one.waitInCurrent
- Queues the new task to run after existing tasks complete
Tasks are isolated by their keys, meaning operations with different keys run concurrently, while operations with the same key are managed according to their mode.
let manager = TaskManagerActor()
// Drop any existing user fetch and start a new one
let task = await manager.task(
key: TaskKey("user-fetch"),
mode: .dropCurrent
) {
let user = try await api.fetchUser()
return user
}
// Wait for the result
let user = try await task.value
class SearchViewModel {
let taskManager = TaskManagerActor()
func search(query: String) async {
// Cancel previous search when user types
await taskManager.task(
key: TaskKey("search"),
mode: .dropCurrent
) {
// Debounce
try await Task.sleep(for: .milliseconds(300))
let results = try await api.search(query)
await MainActor.run {
self.searchResults = results
}
}
}
}
TaskManager provides a property wrapper for seamless SwiftUI integration:
struct UserProfileView: View {
@TaskManager var taskManager
@State private var isLoading = false
@State private var user: User?
var body: some View {
VStack {
if isLoading {
ProgressView()
} else if let user {
Text(user.name)
}
Button("Refresh") {
taskManager.task(
isRunning: $isLoading,
key: TaskKey("fetch-user"),
mode: .dropCurrent
) {
user = try await api.fetchCurrentUser()
}
}
}
}
}
Create sophisticated task isolation strategies:
// Isolate tasks per user
func updateUserStatus(userID: String, isFavorite: Bool) async {
let key = TaskKey(UserOperations.self).combined(userID)
await taskManager.task(key: key, mode: .dropCurrent) {
try await api.updateUserStatus(userID, favorite: isFavorite)
}
}
// Isolate tasks per resource and operation
func downloadImage(url: URL, size: ImageSize) async {
let key = TaskKey("image-download")
.combined(url.absoluteString)
.combined(size.rawValue)
await taskManager.task(key: key, mode: .waitInCurrent) {
try await imageLoader.download(url, size: size)
}
}
Execute multiple operations efficiently:
await taskManager.batch { manager in
// These run concurrently (different keys)
manager.task(key: TaskKey("fetch-user"), mode: .dropCurrent) {
userData = try await api.fetchUser()
}
manager.task(key: TaskKey("fetch-posts"), mode: .dropCurrent) {
posts = try await api.fetchPosts()
}
manager.task(key: TaskKey("fetch-settings"), mode: .dropCurrent) {
settings = try await api.fetchSettings()
}
}
Control task execution flow:
let manager = TaskManagerActor()
// Pause all task execution
await manager.setIsRunning(false)
// Tasks will queue but not execute
await manager.task(key: TaskKey("operation"), mode: .waitInCurrent) {
// This won't run until isRunning is true
}
// Resume execution
await manager.setIsRunning(true)
// Check if a specific task is running
let isRunning = await manager.isRunning(for: TaskKey("operation"))
TaskManager preserves Swift's native error handling:
do {
let result = try await taskManager.task(
key: TaskKey("risky-operation"),
mode: .dropCurrent
) {
try await riskyOperation()
}.value
} catch is CancellationError {
print("Task was cancelled")
} catch {
print("Task failed: \(error)")
}
class UserRepository {
private let taskManager = TaskManagerActor()
func fetchUser(id: String, forceRefresh: Bool = false) async throws -> User {
let key = TaskKey(UserOperations.self).combined(id)
let mode: TaskManagerActor.Mode = forceRefresh ? .dropCurrent : .waitInCurrent
return try await taskManager.task(key: key, mode: mode) {
// Check cache first
if !forceRefresh, let cached = await cache.get(id) {
return cached
}
// Fetch from network
let user = try await api.fetchUser(id)
await cache.set(user, for: id)
return user
}.value
}
}
@Observable
class ProductListViewModel {
private let taskManager = TaskManagerActor()
var products: [Product] = []
var isLoading = false
func loadProducts(category: String? = nil) {
Task {
await taskManager.task(
key: TaskKey("load-products").combined(category ?? "all"),
mode: .dropCurrent
) {
await MainActor.run { self.isLoading = true }
defer { Task { @MainActor in self.isLoading = false } }
let products = try await api.fetchProducts(category: category)
await MainActor.run {
self.products = products
}
}
}
}
}
The main actor that manages task execution.
task(label:key:mode:priority:operation:)
- Submit a task for executiontaskDetached(label:key:mode:priority:operation:)
- Submit a detached taskbatch(_:)
- Execute multiple operations in a batchsetIsRunning(_:)
- Control task execution stateisRunning(for:)
- Check if a task is running for a given keycancelAll()
- Cancel all managed tasks
Identifies and groups related tasks.
init(_:TaskKeyType)
- Create from a typeinit(_:String)
- Create from a stringinit(_:Int)
- Create from an integerinit(_:Hashable & Sendable)
- Create from any hashable value
combined(_:)
- Combine with another keystatic func distinct()
- Create a unique keystatic func code()
- Create a key from source location
Provides TaskManager functionality in SwiftUI views with automatic lifecycle management.
SwiftUI-friendly wrapper with isRunning
binding support.
Contributions are welcome! Please feel free to submit a Pull Request.
TaskManager is available under the Apache 2.0 license. See the LICENSE file for more info.
Built with β€οΈ using Swift's modern concurrency features and inspired by the need for better async task control in real-world applications.