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
- Thread-safe design - Built with manual synchronization for optimal performance
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:columnTaskManager 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 = TaskManager()
// Drop any existing user fetch and start a new one
let task = 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.valueclass SearchViewModel {
let taskManager = TaskManager()
func search(query: String) {
// Cancel previous search when user types
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 {
@LocalTask 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") {
// Using the SwiftUI extension for binding support
taskManager.taskWithBinding(
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) {
let key = TaskKey(UserOperations.self).combined(userID)
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) {
let key = TaskKey("image-download")
.combined(url.absoluteString)
.combined(size.rawValue)
taskManager.task(key: key, mode: .waitInCurrent) {
try await imageLoader.download(url, size: size)
}
}Execute multiple operations concurrently:
// Tasks with different keys run concurrently
taskManager.task(key: TaskKey("fetch-user"), mode: .dropCurrent) {
userData = try await api.fetchUser()
}
taskManager.task(key: TaskKey("fetch-posts"), mode: .dropCurrent) {
posts = try await api.fetchPosts()
}
taskManager.task(key: TaskKey("fetch-settings"), mode: .dropCurrent) {
settings = try await api.fetchSettings()
}Control task execution flow:
let manager = TaskManager()
// Pause all task execution
manager.setIsRunning(false)
// Tasks will queue but not execute
manager.task(key: TaskKey("operation"), mode: .waitInCurrent) {
// This won't run until isRunning is true
}
// Resume execution
manager.setIsRunning(true)
// Check if a specific task is running
let isRunning = 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 = TaskManager()
func fetchUser(id: String, forceRefresh: Bool = false) async throws -> User {
let key = TaskKey(UserOperations.self).combined(id)
let mode: TaskManager.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 = TaskManager()
var products: [Product] = []
var isLoading = false
func loadProducts(category: String? = nil) {
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 class that manages task execution with thread-safe operations.
task(label:key:mode:priority:operation:)- Submit a task for executiontaskDetached(label:key:mode:priority:operation:)- Submit a detached tasksetIsRunning(_:)- Control task execution stateisRunning(for:)- Check if a task is running for a given keycancel(key:)- Cancel tasks for a specific 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. The property wrapper directly exposes a TaskManager instance that is automatically cleaned up when the view is deallocated.
TaskManager includes a SwiftUI-specific extension method taskWithBinding that provides isRunning binding support for tracking task execution state.
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.