Hey everyone,
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, after await, the resumed execution might not return to the Task's original actor. 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 an async 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 tried Task.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 where Task.detached would be a better fit?
class ViewController : 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
}
}
Does calling
fetchImage()
on@MainActor
force it to run entirely on the main thread?
Yes and no.
All the code in fetchImage()
runs on the main thread, but it’s not because you’re calling it from the main actor. Rather, it’s because the method itself is bound to the main actor. That’s because it’s a method in a UIViewController
subclass, and UIViewController
is bound to the main actor.
As to how you fix your issue, the key thing to note here is that fetchImage()
has two parts:
-
It first fetches the image data using
URLSession
. -
It then decodes that data into a
UIImage
.
It’s fine to do part 1 on the main actor because you’re not actually using the CPU. You make as async call into URLSession
and, when it blocks waiting for the network, the main actor becomes available for other work.
OTOH, part 2 is a problem, because decoding an image is CPU intensive and not something you want to do on the main actor. So, you want to move that decoding off the main actor.
An easy way to do that is to create a helper function that do the work:
func image(decoding data: Data) async -> UIImage? {
UIImage(data: data)?.preparingForDisplay()
}
Note the call to preparingForDisplay()
, which forces the decode to happen right now.
IMPORTANT Don’t make this a standard method on your view controller, because that’ll be bound to the main actor. Instead, make it one of:
-
A standalone async function
-
A static async method
-
A
nonisolated
async method -
An async method on a non-actor type that isn’t bound to the main actor
-
A method on an actor that’s not the main actor
[Based on eoonline’s feedback, I edited the list to emphasise that I’m talking about async functions here.]
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"