Struggling with async/await: Fetching an image off the main thread

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:

  1. 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?

  1. 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?

  1. 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
    }
}
Answered by DTS Engineer in 817748022

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:

  1. It first fetches the image data using URLSession.

  2. 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"

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:

  1. It first fetches the image data using URLSession.

  2. 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"

@DTS Engineer My goal isn't just about fetching an image; it's about hitting any API. In the traditional closure-based flow, we manually switch to a background thread to make the API call and then switch back to the main thread to handle the result. I want to achieve the same functionality using structured concurrency,

Folks, This conversation will go more smoothly if you reply as a reply, rather than in the comments. See Quinn’s Top Ten DevForums Tips for this and other tips.


eoonline wrote:

for the sake of future readers, that alone is insufficient.

Agreed. As that’s a critical point of my previous post, I’ve edited that post to clarify it.

Thanks for pointing this out.


Praveenraj4256 wrote:

Using Task.detached is better approach?

No. In general, you should try to avoid creating new tasks wherever possible.

I want to achieve the same functionality using structured concurrency

I think it’s a mistake to look for ‘drop in’ replacements for old techniques. The key thing to remember about Swift concurrency is that the isolation context of your code is determined statically [1]. That’s a benefit for you — you can look at the code and understand its isolation context — but it’s of critical importance to the compiler, because it’s what allows the compiler to check your concurrency in the same way that it checks your types. So, concurrency problems become compiler-time errors rather than horrible runtime debugging issues.

It’s possible to use the old model in Swift 6 mode, but it’s extra work and you miss out on benefits of static concurrency checking. If you’re OK with the latter, just stick with Swift 5 mode.

My second key tip for dealing with Swift concurrency is to avoid mixing concurrency domains in the same type. The example you showed is a view controller, and that’s fundamentally isolated to the main actor. If you put your concurrent code in that type, you end up with one type that lives in multiple concurrency domains, which is just confusing [2].

Instead, put your concurrent code in a different type, one with different isolation. That simplifies both types. And Swift’s async/await model makes it easy to get from one type to the other.

And that advice is good for other reasons too. Putting your networking code in your view controller was a mistake long before Swift concurrency was invented. Gosh, it was a mistake long before Swift was invented (-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Mostly (-: There are exceptions to this general rule.

[2] Indeed, I’ve been suggestion this separation for many years, as illustrated by TN2109 Simple and Reliable Threading with NSOperation from 2010!

Hey Quinn, @DTS Engineer

My bad—initially, I wrote the entire code in the view controller to simplify things (yes, I know, Massive View Controller is not a great practice! LOL). I’ve now restructured the code using a simple VIPER architecture.

The Actual Goal: I want to fetch data from a server off the main thread, including any data conversion needed for the UI, and then update the UI before and after the fetch.

How I Structured It: • The dataTask is triggered inside the interactor’s async fetchListData() function. • In the presenter (which is a class, not an actor), I call fetchListData() using Task. However, since Task doesn’t inherit an actor context, after an await suspension point, it might resume on a different thread. To handle this, I annotated my presenter method with @MainActor to ensure UI updates happen on the main thread. My Questions:

  1. Is using @MainActor in the Task the best practice? Or should I make my entire presenter a MainActor?
  2. If I use @MainActor in the presenter, will the interactor’s fetchListData() always be executed off the main thread? I don’t want to overload the main thread with network calls or data conversion.
  3. Is there a way to ensure that the interactor always returns values on the main thread so that I don’t need to rely on @MainActor annotations in the presenter?

Git repo : https://github.com/praveeniroh/AsynAwaitTest

Struggling with async/await: Fetching an image off the main thread
 
 
Q