The trouble is that Post is main actor isolated by default. One obvious solution is to declare Post nonisolated:
nonisolated
struct Post: Identifiable, Decodable {
Since Post, as you have it, is eminently sendable, this should not pose any new issues: Post will simply work both when it is used from the main actor and when it is used from a background actor.
Another approach, if you know you are always going to decode Post on a background thread, is to declare Post’s conformance to Decodable as itself nonisolated:
struct Post: Identifiable, nonisolated Decodable {
That might be simpler, and I rather prefer it, as being closer to following the SWYM rule (Say What You Mean).
Other points:
This is not what you asked, but the correct modern way to ensure that your task runs on a background thread is not to say Task.detached, which heedlessly removes the task from all forms of structured concurrency, but rather to declare the task operation @concurrent:
Task { @concurrent [weak self] in
Finally, get rid of all your [weak self] declarations. They just waste energy. Tasks almost never need to worry about this sort of things. Try it and see: put a deinit { print("farewell from model") } in your PostViewModel class. You will discover that it was never leaking in the first place. Thus:
Task { @concurrent in
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decoded = try JSONDecoder().decode(Post.self, from: data)
Task { @MainActor in
post = decoded
}
} catch {
print("Failed to load: \(error.localizedDescription)")
}
}