Two small Swift Concurrency tools with big effects. Task.sleep inside .task(id:) debounces
keystroke-driven work, and Task.yield keeps long synchronous loops from starving the
cooperative thread pool.
Debounce with .task(id:) plus Task.sleep
.task(id: query) cancels the previous task and starts a new one whenever query changes.
Alone, that fires a search per keystroke and the searches can race. Sleep first: a cancelled
sleep throws, so intermediate queries never reach the heavy work.
List(store.results, id: \.uuid) { result in
Text(verbatim: result.endDate.formatted())
}
.searchable(text: $query)
.task(id: query) {
do {
try await Task.sleep(for: .seconds(1))
await store.search(matching: query)
} catch {
// cancelled: a new query arrived or the view disappeared
}
}
Typing "apple" runs one search, for "apple", instead of five. There is no built-in debounce function; the sleep-then-work pattern is the idiom.
Yield inside long synchronous loops
The cooperative thread pool has few threads. An async function that grinds through synchronous
work, such as decoding a stack of large JSON files, blocks one of them for minutes and starves
other tasks. Call Task.yield() between chunks to hand the thread back.
struct DataHandler {
func process(json files: [Data]) async throws -> [Item] {
let decoder = JSONDecoder()
var result: [Item] = []
for file in files {
let items = try decoder.decode([Item].self, from: file)
result.append(contentsOf: items)
await Task.yield()
}
return result
}
}
Every await already yields, so manual yielding only matters around non-async APIs such as
JSONDecoder. Add it per iteration, not once at the end.