I have two use cases: offloading heavy work from the UI thread to keep the UI smooth.
extension MoveNoteViewController: UISearchBarDelegate {
// Busy function.
private func filterNotes(_ text: String) async -> [Note] {
let filteredNotes: [Note] = await Task.detached { [weak self] in
guard let self else { return [] }
let idToFolderMap = await idToFolderMap!
if text.isEmpty {
return await notes
} else {
return await notes.filter { [weak self] in
guard let self else { return false }
let emoji = $0.emoji
let title = $0.title
var folderName: String? = nil
if let folderId = $0.folderId {
folderName = idToFolderMap[folderId]?.name ?? ""
}
return
emoji.localizedCaseInsensitiveContains(text) ||
title.localizedCaseInsensitiveContains(text) ||
(folderName?.localizedCaseInsensitiveContains(text) ?? false)
}
}
}.value
return filteredNotes
}
@MainActor
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
let text = searchText.trim()
if text.isEmpty {
applySnapshot(snapshot: getSnapshot(notes: notes))
} else {
Task {
let filteredNotes = await filterNotes(text)
if searchBar.text?.trim() == text {
applySnapshot(snapshot: getSnapshot(notes: filteredNotes))
}
}
}
}
}
// Busy function.
private static func fetchRecentLocalFailedNoteCountAsync() async -> Int {
return await Task.detached { () -> Int in
let fileManager = FileManager.default
guard let enumerator = fileManager.enumerator(at: UploadDataDirectory.audio.url, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]) else { return 0 }
var count = 0
for case let fileURL as URL in enumerator {
if !RecordingUtils.isValidAudioFileExtension(fileURL.pathExtension) {
continue
}
if let fileCreationTimestamp = FileUtils.getFileCreationTimestamp(from: fileURL) {
if await fileCreationTimestamp > MainViewController.createdTimeStampConstraint {
count += 1
}
}
}
return count
}.value
}
I was wondering, am I using Task.detached in a correct and good practice way?
To offload some CPU intensive work from the main thread, there are many ways to accomplish this.
One approach, is defining a global actor specifically for running CPU intensive work. Then, associate CPU intensive functions with it:
Your custom minimalistic Global Actor:
@globalActor actor MyGlobalActor: GlobalActor {
static let shared = MyGlobalActor()
}
CPU intensive synchronous function:
@MyGlobalActor
func compute(input: sending Input) -> sending Output {
...
}
Note: you may require Input
and Output
to conform to Sendable
.
Then, use it like
@MainActor
func foo() async throws {
let result = await compute(input: 100.0)
print(result)
}
Why does this work:
"By default, actors execute tasks on a shared global concurrency thread pool."
The MyGlobalActor
global actor above, does not specify a custom executor.
See: https://developer.apple.com/documentation/swift/actor
Another approach is, to associate the CPU intensive function with an actor instance, which then becomes actor isolated:
func compute(
isolated: isolated any Actor = #isolation,
input: sending Input
) -> sending Output {
...
}
You then call it like:
Task { @MyGlobalActor in
let result = compute(input: 100.0)
print(result)
}
The actor instance will be implicitly passed as an argument. The compute()
function executes on MyGlobalActor
.
Note, that you don't need to use a Task and a closure which specifies the actor. It's sufficient, that the compiler can infer an actor. Note also, that the compiler requires to infer the actor, otherwise it ungracefully fails to compile (I hope this will be improved in the future).
One can make the parameter isolated
also optional. In this case, the compiler wouldn't crash when there is no actor. However, I would rather prefer the compiler to crash, to indicate there's an actor missing, than running the code on an unspecified isolation domain, which would missing the original goal.