In this code-along, we build a tiny SwiftUI weather app that loads the current temperature for Zurich using a real weather API. The app is intentionally small: one screen, one API call, and a few focused code snippets.
The goal is not to build a full weather app. The goal is to understand why Swift Concurrency matters and how async/await helps us write cleaner, safer code when our app needs to wait for something – like data coming from the internet.
Why Swift Concurrency matters
Modern apps constantly perform tasks that take time:
- loading data from an API
- reading or saving files
- talking to a database
- processing images
- using AI models
- updating the UI after background work
- background calculations
Imagine a weather app that requests current data from a server. If the app had to completely stop and wait for the server response, the interface could freeze. Buttons would feel unresponsive, animations would stutter, and the experience would feel broken.
Concurrency solves this problem.
While the network request is in progress, the app can still:
- respond to taps
- scroll smoothly
- animate views
- show a loading indicator
- cancel tasks
- continue other independent work
That is why concurrency is one of the most important concepts in modern app development: it helps apps feel fast, fluid, and professional.
Why we need concurrency in this app
A weather request does not return instantly. The app needs to contact Open-Meteo, wait for the response, decode the JSON, and then update the screen.
That waiting part is exactly where Swift Concurrency helps.
Instead of writing nested callbacks, we can write code that reads almost like normal synchronous Swift:
let weather = try await weatherService.fetchCurrentWeather()
The API: Open-Meteo
For this tutorial, we use Open-Meteo because it is free for non-commercial use and does not require an API key. That means readers can copy the code and try it immediately.
Example request for Zurich:
https://api.open-meteo.com/v1/forecast?latitude=47.37&longitude=8.54¤t=temperature_2m&timezone=auto
The important parts are:
latitude=47.37longitude=8.54current=temperature_2mtimezone=auto
Open-Meteo’s forecast endpoint accepts geographical coordinates and weather variables, including current conditions.
Step 1: Create the weather model
The API returns JSON. We need Swift types that match the response.
struct WeatherResponse: Decodable {
let current: CurrentWeather
}
struct CurrentWeather: Decodable {
let temperature2m: Double
enum CodingKeys: String, CodingKey {
case temperature2m = "temperature_2m"
}
}
WeatherResponse represents the full response from Open-Meteo.
CurrentWeather represents the current weather section.
The API uses the name temperature_2m, but Swift properties usually use camelCase. That is why we map it with CodingKeys.
Step 2: Create a weather service
Now we create a small service that loads the weather data.
struct WeatherService {
func fetchCurrentWeather() async throws -> CurrentWeather {
let url = URL(string: "https://api.open-meteo.com/v1/forecast?latitude=47.37&longitude=8.54¤t=temperature_2m&timezone=auto")!
let (data, _) = try await URLSession.shared.data(from: url)
let decodedResponse = try JSONDecoder().decode(WeatherResponse.self, from: data)
return decodedResponse.current
}
}
This function is marked with async because loading data from the internet takes time.
It is marked with throws because something could go wrong:
- no internet connection
- invalid server response
- decoding error
- wrong URL structure
The most important line is:
let (data, _) = try await URLSession.shared.data(from: url)
This tells Swift: “Start the network request and wait for the data — but keep the app responsive.”
That is Swift Concurrency in action.
Step 3: Create the observable state
Next, we create a small store for the screen.
@Observable
class WeatherStore {
var temperature: Double?
var isLoading = false
var errorMessage: String?
private let service = WeatherService()
func loadWeather() async {
isLoading = true
errorMessage = nil
do {
let weather = try await service.fetchCurrentWeather()
temperature = weather.temperature2m
} catch {
errorMessage = "Could not load weather."
}
isLoading = false
}
}
WeatherStore holds everything the UI needs:
- the temperature
- a loading state
- an error message
The loadWeather() function is also async because it calls another async function:
let weather = try await service.fetchCurrentWeather()
This creates a simple chain:
SwiftUI View → WeatherStore → WeatherService → Open-Meteo
Step 4: Build the SwiftUI view
Now we show the result on screen.
import SwiftUI
import Observation
struct ContentView: View {
@State private var store = WeatherStore()
var body: some View {
VStack(spacing: 20) {
Text("Zurich Weather")
.font(.title)
if store.isLoading {
ProgressView("Loading...")
} else if let temperature = store.temperature {
Text("\(temperature, specifier: "%.1f") °C")
.font(.largeTitle)
} else if let errorMessage = store.errorMessage {
Text(errorMessage)
.foregroundStyle(.red)
}
Button("Refresh") {
Task {
await store.loadWeather()
}
}
}
.padding()
.task {
await store.loadWeather()
}
}
}
The .task modifier starts loading the weather when the view appears:
.task {
await store.loadWeather()
}
The button starts a new asynchronous task when the user taps Refresh:
Button("Refresh") {
Task {
await store.loadWeather()
}
}
This is important because button actions themselves are not async. Task gives us a place where we can call async code.
Step 5: Why this works so nicely
Swift Concurrency helps us keep three things clear:
- What takes time
func fetchCurrentWeather() async throws -> CurrentWeatherThe
asynckeyword tells us immediately: this function waits for something. - Where we wait
try await URLSession.shared.data(from: url)The
awaitkeyword makes the waiting point visible. - How the UI stays responsive
While the app waits for Open-Meteo, SwiftUI can still update, animate, and respond to the user.
That is the big advantage: the app does not freeze.
Optional next step: Load more data in parallel
Why concurrency becomes even more powerful:
Imagine loading current weather and air quality at the same time:
async let weather = service.fetchCurrentWeather()
async let airQuality = service.fetchAirQuality()
let result = try await (weather, airQuality)
With async let, both requests can start together. The app does not need to wait for one to finish before starting the other.
That is a simple but powerful idea: independent tasks can run in parallel.
What you learned
In this code-along, you learned how Swift Concurrency helps you load real data from the internet while keeping your app responsive.
You used:
asyncto mark work that takes timeawaitto wait for the resultthrowsto handle possible errorsURLSessionto call a real APICodableto decode JSON@Observableto update SwiftUI when data changesTaskto start async work from a button
A small weather app — but a very important Swift concept.
That’s a wrap!
Keep learning, keep building, and let your curiosity guide you.
Happy coding! ✨
When you have exhausted all possibilities, remember this: you haven’t. – Thomas Edison

