I’m learning async/await
and trying to fetch an image from a URL off the main thread to avoid overloading it, while updating the UI afterward. Before starting the fetch, I want to show a loading indicator (UI-related work). I’ve implemented this in two different ways using Task
and Task.detached
, and I have some doubts:
-
Is using
Task { @MainActor
the better approach?
I added@MainActor
because, afterawait
, the resumed execution might not return to the Task’s original thread. Is this the right way to ensure UI updates are done safely? -
Does calling
fetchImage()
on@MainActor
force it to run entirely on the main thread?
I used anasync
data fetch function (not explicitly marked with any actor). If I were to use a completion handler instead, would the function run on the main thread? -
Is using
Task.detached
overkill here?
I triedTask.detached
to ensure the fetch runs on a non-main actor. However, it seems to involve unnecessary actor hopping since I still need to hop back to the main actor for UI updates. Is there any scenario whereTask.detached
would be a better fit?
Any guidance on best practices for these scenarios would be greatly appreciated. Thanks in advance!
class MyViewController : UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
//MARK: First approch
Task{@MainActor in
showLoading()
let image = try? await fetchImage() //Will the image fetch happen on main thread?
updateImageView(image:image)
hideLoading()
}
//MARK: 2nd approch
Task{@MainActor in
showLoading()
let detachedTask = Task.detached{
try await self.fetchImage()
}
updateImageView(image:try? await detachedTask.value)
hideLoading()
}
}
func fetchImage() async throws -> UIImage {
let url = URL(string: "https://via.placeholder.com/600x400.png?text=Example+Image")!
//Async data function call
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
guard let image = UIImage(data: data) else {
throw URLError(.cannotDecodeContentData)
}
return image
}
func showLoading(){
//Show Loader handling
}
func hideLoading(){
//Hides the loader
}
func updateImageView(image:UIImage?){
//Image view updated
}
}
2
Answers
Yes you want to use detached task in this case to get the other task out of the main actor. per Apple documentation unstructured concurrency
I know this was answered on the Apple Developer Forums, but just to capture the salient parts of Quinn’s answer here:
The
@MainActor
inTask { @MainActor in … }
is unnecessary.UIViewController
is isolated to the main actor, and therefore so areMyViewController
and its methods, includingviewDidLoad
.Thus it is just:
The
fetchImage
method is isolated to the main actor (for reasons outlined above), but the fetching of the image is OK because youawait
anasync
function:This will
await
thedata(from:)
call and will not block the main thread. No problem here.What is potentially problematic in
fetchImage
is the call toUIImage(data:)
:That can block the main actor momentarily. So we would generally want to move that off the current actor. So, you could theoretically use a detached task:
That having been said, we generally try to avoid unstructured concurrency, so this is a common pattern:
This
nonisolated
async
pattern is a nice, concise way to move work off the main actor. Other structured concurrency approaches include using a separate actor for this work.In this case (with non-cancellable work), the difference between the detached task and the structured concurrency is not terribly critical. (We remain with structured concurrency to enjoy, amongst other things, automatic cancelation propagation.) But unstructured concurrency has a hint of code smell, and you might consider, as a rule, remaining with structured concurrency where you can.
Two unrelated observation:
I suspect you were simplifying the example for the sake of a MRE, but we would generally avoid
try?
unless we really wanted to ignore errors. So, perhaps:I might advise against manually throwing
URLError
types ofbadServerResponse
andcannotDecodeContentData
. I confess that I have done this myself in throw-away examples, but these error codes mean something different than what you have here. We would generally use our own custom errors, e.g.: