Search code examples
swiftmvvmasync-awaitstructured-concurrency

Where should we use "Task {}": in ViewModel or ViewController?


Let's suppose we have some asynchronous code. At some point we must wrap it in a Task {…} in order to run it from a synchronous context. So where is the canonical way to do so? ViewModel or ViewController?

If we wrap it with Task {…} in ViewModel, the ViewModel functions become effectively synchronous, and calling them from ViewController will still require all those completions/delegates/closures/RXs dance to accomplish some UI updates after asynchronous work finishes.

In the other hand, if we mark ViewModel functions as async and call them from ViewController within Task {…} body, it seems to solve the problem. So is it a way to go?


Solution

  • I would not re-introduce legacy completion patterns (closures, delegates, etc.). That defeats the purpose of Swift concurrency, to gracefully manage asynchronous dependencies. E.g., in WWDC 2021 video Swift concurrency: Update a sample app, they show example(s) of how we eliminate completion handlers with Swift concurrency.

    So, designate the asynchronous view model methods as async. Then, the view controller will use Task {…} to enter the asynchronous context of Swift concurrency so that it can await the async method of the view model and then trigger the UI update when it is done.

    But, use of Task {…} is not limited to the view controller. The view model may use it, too (e.g., where one needs to save a task in order to possibly cancel it later for some reason).

    But you ask:

    if we mark ViewModel functions as async and call them from ViewController within Task {…} body, it seems to solve the problem. So is it a way to go?

    Precisely. If the method is really doing something asynchronous, then mark it as async and the view controller will invoke it from within a Task {…}.


    All of that having been said, in SwiftUI projects, where the view model often communicates updates to the view with ObservableObject and @Published properties, I would remain within that intuitive and natural pattern. At that point, where you choose to cross into the asynchronous context becomes a little less compelling/critical.

    That having been said, from a testability perspective, though, you still want to be able to know when the view model’s asynchronous task is done, so I would probably still make the view model’s methods async.