Skip to content

Commit 87605b6

Browse files
muukiiclaude
andauthored
Refactor: Convert TaskManagerActor to class and simplify SwiftUI integration (#13)
## Summary - Converted TaskManagerActor from actor to final class with manual synchronization for better performance - Renamed TaskManagerActor to TaskManager for simplicity - Renamed SwiftUI property wrapper from TaskManager to LocalTask to avoid naming conflicts - Removed LocalTaskWrapper, exposing TaskManager directly from the property wrapper ## Changes ### Core Refactoring - **TaskNode**: Converted from class to struct with OSAllocatedUnfairLock for thread-safe state management - **TaskManager** (formerly TaskManagerActor): Converted from actor to class with Sendable conformance - Uses OSAllocatedUnfairLock for manual synchronization - Removed actor overhead for better performance - Removed batch methods as they're no longer needed ### File Organization - Extracted TaskNode into separate file (`TaskNode.swift`) - Extracted AutoReleaseContinuationBox into separate file (`AutoReleaseContinuationBox.swift`) ### SwiftUI Integration - Renamed @taskmanager property wrapper to @localtask - Removed LocalTaskWrapper intermediate layer - Direct exposure of TaskManager instance from property wrapper - Simplified API surface ## Test Results All tests passing ✅ ## Breaking Changes - `TaskManagerActor` renamed to `TaskManager` - SwiftUI property wrapper renamed from `@TaskManager` to `@LocalTask` - Batch methods removed 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude <[email protected]>
1 parent adabcdc commit 87605b6

17 files changed

+809
-1044
lines changed

.claude/settings.local.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"mcp__swiftlens__swift_get_symbols_overview",
5+
"Bash(git commit:*)",
6+
"Bash(git mv:*)"
7+
],
8+
"deny": [],
9+
"ask": []
10+
}
11+
}

Package.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import PackageDescription
66
let package = Package(
77
name: "swift-concurrency-task-manager",
88
platforms: [
9-
.macOS(.v11),
10-
.iOS(.v14),
9+
.macOS(.v13),
10+
.iOS(.v16),
1111
.tvOS(.v16),
1212
.watchOS(.v10)
1313
],
@@ -35,5 +35,5 @@ let package = Package(
3535
dependencies: ["ConcurrencyTaskManager"]
3636
),
3737
],
38-
swiftLanguageModes: [.v5, .v6]
38+
swiftLanguageModes: [.v6]
3939
)

README.md

Lines changed: 61 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ TaskManager solves this by providing:
1414
- **Task isolation by key** - Group related operations together
1515
- **Execution control** - Choose whether to cancel existing tasks or queue new ones
1616
- **SwiftUI integration** - First-class support for UI-driven async operations
17-
- **Actor-based safety** - Thread-safe by design using Swift actors
17+
- **Thread-safe design** - Built with manual synchronization for optimal performance
1818

1919
## 🚀 Installation
2020

@@ -79,10 +79,10 @@ Tasks are isolated by their keys, meaning operations with different keys run con
7979
### Simple Task Management
8080

8181
```swift
82-
let manager = TaskManagerActor()
82+
let manager = TaskManager()
8383

8484
// Drop any existing user fetch and start a new one
85-
let task = await manager.task(
85+
let task = manager.task(
8686
key: TaskKey("user-fetch"),
8787
mode: .dropCurrent
8888
) {
@@ -98,17 +98,17 @@ let user = try await task.value
9898

9999
```swift
100100
class SearchViewModel {
101-
let taskManager = TaskManagerActor()
102-
103-
func search(query: String) async {
101+
let taskManager = TaskManager()
102+
103+
func search(query: String) {
104104
// Cancel previous search when user types
105-
await taskManager.task(
105+
taskManager.task(
106106
key: TaskKey("search"),
107107
mode: .dropCurrent
108108
) {
109109
// Debounce
110110
try await Task.sleep(for: .milliseconds(300))
111-
111+
112112
let results = try await api.search(query)
113113
await MainActor.run {
114114
self.searchResults = results
@@ -124,20 +124,21 @@ TaskManager provides a property wrapper for seamless SwiftUI integration:
124124

125125
```swift
126126
struct UserProfileView: View {
127-
@TaskManager var taskManager
127+
@LocalTask var taskManager
128128
@State private var isLoading = false
129129
@State private var user: User?
130-
130+
131131
var body: some View {
132132
VStack {
133133
if isLoading {
134134
ProgressView()
135135
} else if let user {
136136
Text(user.name)
137137
}
138-
138+
139139
Button("Refresh") {
140-
taskManager.task(
140+
// Using the SwiftUI extension for binding support
141+
taskManager.taskWithBinding(
141142
isRunning: $isLoading,
142143
key: TaskKey("fetch-user"),
143144
mode: .dropCurrent
@@ -158,44 +159,42 @@ Create sophisticated task isolation strategies:
158159

159160
```swift
160161
// Isolate tasks per user
161-
func updateUserStatus(userID: String, isFavorite: Bool) async {
162+
func updateUserStatus(userID: String, isFavorite: Bool) {
162163
let key = TaskKey(UserOperations.self).combined(userID)
163-
164-
await taskManager.task(key: key, mode: .dropCurrent) {
164+
165+
taskManager.task(key: key, mode: .dropCurrent) {
165166
try await api.updateUserStatus(userID, favorite: isFavorite)
166167
}
167168
}
168169

169170
// Isolate tasks per resource and operation
170-
func downloadImage(url: URL, size: ImageSize) async {
171+
func downloadImage(url: URL, size: ImageSize) {
171172
let key = TaskKey("image-download")
172173
.combined(url.absoluteString)
173174
.combined(size.rawValue)
174-
175-
await taskManager.task(key: key, mode: .waitInCurrent) {
175+
176+
taskManager.task(key: key, mode: .waitInCurrent) {
176177
try await imageLoader.download(url, size: size)
177178
}
178179
}
179180
```
180181

181-
### Batch Operations
182+
### Concurrent Operations
182183

183-
Execute multiple operations efficiently:
184+
Execute multiple operations concurrently:
184185

185186
```swift
186-
await taskManager.batch { manager in
187-
// These run concurrently (different keys)
188-
manager.task(key: TaskKey("fetch-user"), mode: .dropCurrent) {
189-
userData = try await api.fetchUser()
190-
}
191-
192-
manager.task(key: TaskKey("fetch-posts"), mode: .dropCurrent) {
193-
posts = try await api.fetchPosts()
194-
}
195-
196-
manager.task(key: TaskKey("fetch-settings"), mode: .dropCurrent) {
197-
settings = try await api.fetchSettings()
198-
}
187+
// Tasks with different keys run concurrently
188+
taskManager.task(key: TaskKey("fetch-user"), mode: .dropCurrent) {
189+
userData = try await api.fetchUser()
190+
}
191+
192+
taskManager.task(key: TaskKey("fetch-posts"), mode: .dropCurrent) {
193+
posts = try await api.fetchPosts()
194+
}
195+
196+
taskManager.task(key: TaskKey("fetch-settings"), mode: .dropCurrent) {
197+
settings = try await api.fetchSettings()
199198
}
200199
```
201200

@@ -204,21 +203,21 @@ await taskManager.batch { manager in
204203
Control task execution flow:
205204

206205
```swift
207-
let manager = TaskManagerActor()
206+
let manager = TaskManager()
208207

209208
// Pause all task execution
210-
await manager.setIsRunning(false)
209+
manager.setIsRunning(false)
211210

212211
// Tasks will queue but not execute
213-
await manager.task(key: TaskKey("operation"), mode: .waitInCurrent) {
212+
manager.task(key: TaskKey("operation"), mode: .waitInCurrent) {
214213
// This won't run until isRunning is true
215214
}
216215

217216
// Resume execution
218-
await manager.setIsRunning(true)
217+
manager.setIsRunning(true)
219218

220219
// Check if a specific task is running
221-
let isRunning = await manager.isRunning(for: TaskKey("operation"))
220+
let isRunning = manager.isRunning(for: TaskKey("operation"))
222221
```
223222

224223
### Error Handling
@@ -246,18 +245,18 @@ do {
246245

247246
```swift
248247
class UserRepository {
249-
private let taskManager = TaskManagerActor()
250-
248+
private let taskManager = TaskManager()
249+
251250
func fetchUser(id: String, forceRefresh: Bool = false) async throws -> User {
252251
let key = TaskKey(UserOperations.self).combined(id)
253-
let mode: TaskManagerActor.Mode = forceRefresh ? .dropCurrent : .waitInCurrent
254-
252+
let mode: TaskManager.Mode = forceRefresh ? .dropCurrent : .waitInCurrent
253+
255254
return try await taskManager.task(key: key, mode: mode) {
256255
// Check cache first
257256
if !forceRefresh, let cached = await cache.get(id) {
258257
return cached
259258
}
260-
259+
261260
// Fetch from network
262261
let user = try await api.fetchUser(id)
263262
await cache.set(user, for: id)
@@ -272,23 +271,21 @@ class UserRepository {
272271
```swift
273272
@Observable
274273
class ProductListViewModel {
275-
private let taskManager = TaskManagerActor()
274+
private let taskManager = TaskManager()
276275
var products: [Product] = []
277276
var isLoading = false
278-
277+
279278
func loadProducts(category: String? = nil) {
280-
Task {
281-
await taskManager.task(
282-
key: TaskKey("load-products").combined(category ?? "all"),
283-
mode: .dropCurrent
284-
) {
285-
await MainActor.run { self.isLoading = true }
286-
defer { Task { @MainActor in self.isLoading = false } }
287-
288-
let products = try await api.fetchProducts(category: category)
289-
await MainActor.run {
290-
self.products = products
291-
}
279+
taskManager.task(
280+
key: TaskKey("load-products").combined(category ?? "all"),
281+
mode: .dropCurrent
282+
) {
283+
await MainActor.run { self.isLoading = true }
284+
defer { Task { @MainActor in self.isLoading = false } }
285+
286+
let products = try await api.fetchProducts(category: category)
287+
await MainActor.run {
288+
self.products = products
292289
}
293290
}
294291
}
@@ -297,17 +294,17 @@ class ProductListViewModel {
297294

298295
## 📚 API Reference
299296

300-
### TaskManagerActor
297+
### TaskManager
301298

302-
The main actor that manages task execution.
299+
The main class that manages task execution with thread-safe operations.
303300

304301
#### Methods
305302

306303
- `task(label:key:mode:priority:operation:)` - Submit a task for execution
307304
- `taskDetached(label:key:mode:priority:operation:)` - Submit a detached task
308-
- `batch(_:)` - Execute multiple operations in a batch
309305
- `setIsRunning(_:)` - Control task execution state
310306
- `isRunning(for:)` - Check if a task is running for a given key
307+
- `cancel(key:)` - Cancel tasks for a specific key
311308
- `cancelAll()` - Cancel all managed tasks
312309

313310
### TaskKey
@@ -329,13 +326,13 @@ Identifies and groups related tasks.
329326

330327
### SwiftUI Components
331328

332-
#### @TaskManager Property Wrapper
329+
#### @LocalTask Property Wrapper
333330

334-
Provides TaskManager functionality in SwiftUI views with automatic lifecycle management.
331+
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.
335332

336-
#### TaskManagerActorWrapper
333+
#### Extension Methods
337334

338-
SwiftUI-friendly wrapper with `isRunning` binding support.
335+
TaskManager includes a SwiftUI-specific extension method `taskWithBinding` that provides `isRunning` binding support for tracking task execution state.
339336

340337
## 🤝 Contributing
341338

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import Foundation
2+
3+
final class AutoReleaseContinuationBox<T>: @unchecked Sendable {
4+
5+
private let lock = NSRecursiveLock()
6+
7+
private var continuation: UnsafeContinuation<T, Error>?
8+
private var wasConsumed: Bool = false
9+
10+
init(_ value: UnsafeContinuation<T, Error>?) {
11+
self.continuation = value
12+
}
13+
14+
deinit {
15+
resume(throwing: CancellationError())
16+
}
17+
18+
func setContinuation(_ continuation: UnsafeContinuation<T, Error>?) {
19+
lock.lock()
20+
defer {
21+
lock.unlock()
22+
}
23+
24+
self.continuation = continuation
25+
}
26+
27+
func resume(throwing error: sending Error) {
28+
lock.lock()
29+
defer {
30+
lock.unlock()
31+
}
32+
guard wasConsumed == false else {
33+
return
34+
}
35+
wasConsumed = true
36+
continuation?.resume(throwing: error)
37+
}
38+
39+
func resume(returning value: sending T) {
40+
lock.lock()
41+
defer {
42+
lock.unlock()
43+
}
44+
guard wasConsumed == false else {
45+
return
46+
}
47+
wasConsumed = true
48+
continuation?.resume(returning: value)
49+
}
50+
51+
}

0 commit comments

Comments
 (0)