Let's think about it this way for a second
`Task { }` is a 'container' for starting asynchronous work — inside a SwiftUI View, you'll typically use the `.task { }` modifier (which runs automatically when the View appears, essentially a shortcut combining `.onAppear` + `Task`). Annotating a class/function with `@MainActor` tells the compiler 'only run this code on the UI thread' — since `@Published` property updates (from the ViewModel lesson) must happen on the main thread, ViewModel classes are usually annotated with `@MainActor`.
Let's connect this to a real-world scenario
Instead of writing `.onAppear { Task { ... } }` in `WeatherScreen`, it's cleaner to write `.task { weather = try? await fetchWeather(city: "Yangon") }` — the `.task` modifier automatically cancels when the View disappears (with `.onAppear` + a manual `Task`, you'd have to write the cancel logic yourself).
Let's look at it together
@MainActor
class WeatherViewModel: ObservableObject {
@Published var weather: WeatherResponse?
func loadWeather(city: String) async {
weather = try? await fetchWeather(city: city)
}
}
struct WeatherScreen: View {
@StateObject private var viewModel = WeatherViewModel()
var body: some View {
Text(viewModel.weather?.city ?? "Loading...")
.task {
await viewModel.loadWeather(city: "Yangon")
}
}
}As soon as you land on the screen, you'll see it automatically update from 'Loading...' to the weather city name.5-minute try-it
Write `WeatherViewModel`/`WeatherScreen` yourself using the `.task { }` modifier — try navigating away from the screen right away and observe `.task`'s auto-cancel behavior.
A quick heads-up
Overusing `try?` inside a `Task { }` without catching errors (silent failure) means neither the user nor the developer knows an error even happened, which makes debugging much harder.