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