Architecture for async/await in an app

Hi!

I have an app that I'd love to update to take advantage of the new swift concurrency features. I feel decently confident with adopting it in the app, though I'd love to hear thoughts from those that are more experienced than me. Currently the app is set up with the following architecture:

  • View Model
    • Typical view model logic
    • Call repositories to load/save data
  • Repository
    • Determine how to load data
      • If online, use web clients
      • If offline, use the local database
  • Web Client
    • Calls the endpoints and deserializes the data

View models load data from repositories by doing something like the following:

@Published var foo: Foo?
@Published var barA: Bar?
@Published var barB: Bar?
var repositoryA = RepositoryA()
var repositoryB = RepositoryB()

func load() async {
    do {
        async let loadFoo = try await repositoryA.loadFoo()
        async let loadBarA = try await repositoryA.loadBar()
        async let loadBarB = try await repositoryB.loadBar()

        let (foo, barA, barB) = try await (loadFoo, loadBarA, loadBarB)
    } catch {
        print(error)
    }
}

My first step in adopting the new features was to mark view models with the @MainActor attribute to ensure all properties the views depend on are set on the main thread.

I'd love some insight on the best way to have any repository calls not be performed on the MainActor. I think there's three different ways:

  1. Repositories are actors

This works seamlessly. However I'm not using them to protect mutable data, so I wonder if using actors is the right approach. I believe it should be correct as this would create new actors/threads that are not MainActor/the main thread to execute code on.

  1. Create a global actor to execute repository code on
@BackgroundActor var repositoryA = RepositoryA()
@BackgroundActor var repositoryB = RepositoryB()

I don't believe this has much of an advantage over option #1. It also forces any parallel calls to multiple actors to execute on the same actor which may not be the best for performance.

  1. Task.init with a specific priority

The load() function could create a Task with a lower priority to execute the async code. However I believe this is still running on the MainActor which is less than desirable.

Task(priority: medium) {
    // The rest of the load() here
}

Though this leads to the code being a bit verbose if every async method in every view model needs to be wrapped in this.

I think the clear solution is option #1 but I'd love to hear any other thoughts or other options I may have missed! Thanks!

As for 1. AFAIK, actors are meant for shared mutable data, since you've mentioned your data is immutable, maybe you can go for structs.
For 3. I'm really not sure yet, but won't you be befitting from Task.detached?