swiftmultithreadingconcurrencyswift-concurrencyswift6

Swift 6 Concurrency: Is Using @MainActor on the Whole ViewModel a Good Practice?


The following SwiftUI app fetches posts from a remote JSON API using Swift Concurrency (async/await). The PostViewModel is marked entirely with @MainActor to avoid data race warnings introduced in Swift 6.

import SwiftUI

struct Post: Identifiable, Codable {
    let userId: Int
    let id: Int
    let title: String
    let body: String
}

@Observable
@MainActor
class PostViewModel {
    var posts: [Post] = []
    var errorMessage: String?
    var isLoading: Bool = false

    func fetchPosts() async {
        isLoading = true
        errorMessage = nil

        guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
            errorMessage = "Invalid URL"
            isLoading = false
            return
        }

        do {
            let (data, response) = try await URLSession.shared.data(from: url)

            guard let httpResponse = response as? HTTPURLResponse,
                  (200...299).contains(httpResponse.statusCode) else {
                errorMessage = "Invalid server response"
                isLoading = false
                return
            }

            let decodedPosts = try JSONDecoder().decode([Post].self, from: data)
            self.posts = decodedPosts
            self.isLoading = false

        } catch {
            errorMessage = "Error fetching data: \(error.localizedDescription)"
            isLoading = false
        }
    }
}

struct PostsListView: View {
    @State var viewModel = PostViewModel()

    var body: some View {
        NavigationStack {
            Group {
                if viewModel.isLoading {
                    ProgressView("Fetching Posts...")
                } else if let errorMessage = viewModel.errorMessage {
                    Text("Error: \(errorMessage)")
                        .foregroundColor(.red)
                } else {
                    List(viewModel.posts) { post in
                        VStack(alignment: .leading) {
                            Text(post.title)
                                .font(.headline)
                            Text(post.body)
                                .font(.subheadline)
                                .foregroundColor(.gray)
                        }
                    }
                }
            }
            .navigationTitle("Posts")
            .task {
                await viewModel.fetchPosts()
            }
        }
    }
}

Is it ideal to mark the entire PostViewModel class with @MainActor for avoiding data races?

If you tried wrapping only the assignment blocks like this:

let decoded = try JSONDecoder().decode([Post].self, from: data)
await MainActor.run {
    self.posts = decoded
    self.isLoading = false
}

but still faced data race warnings — Until i marked while ViewModel as @MainActor?

If marking PostViewModel with @MainActor force to execute all code on main thread isn't it killing the perpose of concurrency ? If yes how would you refactor fetchPosts() to run background tasks (like networking and decoding) off the main thread, while still safely updating observable state on the main thread?


Solution

  • Is it ideal to mark the entire PostViewModel class with @MainActor

    Yes.

    If marking PostViewModel with @MainActor force to execute all code on main thread isn't it killing the perpose of concurrency ? If yes how would you refactor fetchPosts() to run background tasks (like networking and decoding) off the main thread

    Networking does run off the main thread, just as it always has. That is why you have to say try await URLSession.shared.data: you are awaiting the networking which is taking place in the background. And the fact that "you" are awaiting this on the main actor does not block the main actor (because await is magic), so no harm done.

    If decoding really takes an unconscionably long time, you could also make an actor dedicated to decoding. But this would not be the ViewModel; it would be some actor dedicated only to decoding. I think you'll find, however, that decoding takes no time at all and this isn't worth worrying about.