Let's think about it this way for a second
In a network-based app, you have to handle three states in the UI — loading (data is being fetched), success (data has arrived), and error (the network call failed). The user should always know 'what's going on' — you can't just leave a blank screen. A common pattern for managing this is `enum WeatherUiState { case loading; case success(WeatherResponse); case error(String) }` (similar to Android's sealed class pattern).
Let's connect this to a real-world scenario
When you type a city name into the TextField and tap the Search button — the ViewModel sets `state = .loading` and calls `fetchWeather(city:)` inside a `Task { }`. On success it sets `state = .success(response)`; on failure (network down, city not found) it sets `state = .error("...")`. The View then renders differently depending on the state with `switch state { case .loading: ProgressView(); case .success(let data): WeatherCard(data); case .error(let msg): Text(msg) }`.
Let's walk through it together
enum WeatherUiState {
case loading
case success(WeatherResponse)
case error(String)
}
@MainActor
class WeatherViewModel: ObservableObject {
@Published var state: WeatherUiState = .loading
func search(city: String) async {
state = .loading
do {
let weather = try await fetchWeather(city: city)
state = .success(weather)
} catch {
state = .error("Could not load weather for \(city)")
}
}
}Type in a city name and search, and you should see a loading spinner followed by the weather data (or an error message) on screen.5-minute try-it
Build the Weather app yourself (search TextField + Loading/Success/Error UI) — try typing an invalid city name to trigger the error state.
A quick word of caution
Watch out for API rate limits (free-tier weather APIs usually cap requests per day) — repeatedly sending requests while testing can easily blow past the limit.