Search code examples
swiftconcurrencycombine

Best practice with asynchronous functions Swift & Combine


I'm converting my Swift app to use Combine as well as async/await and I'm trying to understand what's the best way to handle interactions between asynchronous functions and the main thread.

Here's an asynchronous function that loads a user:

class AccountManager {

    static func fetchOrLoadUser() async throws -> AppUser {
        if let user = AppUser.current.value {
            return user
        }
    
        let syncUser = try await loadUser()
        let user = try AppUser(syncUser: syncUser)
    
        AppUser.current.value = user // [warning]: "Publishing changes from background threads is not allowed"
        return user
    }
}

And a class:

class AppUser {

    static var current = CurrentValueSubject<AppUser?,Never>(nil)

    // ...
}

Note: I chose to use CurrentValueSubject because it allows me to both (1) read this value synchronously whenever I need it and (2) subscribe for changes.

Now, on the line marked above I get the error Publishing changes from background threads is not allowed, which I understand. I see different ways to solve this issue:

1. Mark whole AccountManager class as @MainActor

Since most of the work done in asynchronous functions is to wait for network results, I'm wondering if there is an issue with simply running everything on the main thread. Would that cause performance issues or not?

2. Englobe error line in DispatchQueue.main.sync

Is that a reasonable solution, or would that cause threading problems like deadlocks?

3. Use DispatchGroup with enter(), leave() and wait()

Like in this answer. Is there a difference at all with solution #2? Because this solution needs more lines of code so I'd rather not use it if possible —I prefer clean code.


Solution

  • You can wrap the call in an await MainActor.run { } block. I think this is the most Swifty way of doing that.

    You should not use the Dispatch mechanism while using Swift Concurrency, even though I think DispatchQueue.main.async { } is safe to use here.

    The @MainActor attribute is safe but shouldn’t be used on an ObservableObject and could potentially slow down the UI if CPU-bound code is run in a method of the annotated type.