Most apps talk to a server. A weather app asks for the forecast, a shop app asks for products, a chat app sends your message to a language model. They all do it the same way: send an HTTP request, get back JSON, and turn the JSON into Swift values. In this lesson you build a small weather screen with the two tools Apple gives you for that, URLSession and Codable.
JSON and Codable
JSON is a plain-text format of keys and values. Codable is a Swift protocol (in fact a pair, Encodable and Decodable) that lets a struct turn into JSON and back, with almost no code. If the property names match the keys in the JSON, you are done. If they do not, you describe the difference with CodingKeys.
import Foundation
struct Weather: Codable {
struct Current: Codable {
let temperature: Double
let windSpeed: Double
enum CodingKeys: String, CodingKey {
case temperature = "temperature_2m"
case windSpeed = "wind_speed_10m"
}
}
let current: Current
}
let json = """
{ "current": { "temperature_2m": 18.4, "wind_speed_10m": 9.1 } }
"""
do {
let weather = try JSONDecoder().decode(Weather.self, from: Data(json.utf8))
print(weather.current.temperature) // 18.4
} catch {
print("Could not read the JSON: \(error)")
}
The three quote marks make a multi-line string. Data(json.utf8) turns text into the raw bytes that the decoder reads. Decoding can fail, for example if a key is missing or has the wrong type, so it is called with try. Fail-with-a-message is much better than a crash, so always keep it inside do and catch, or in a function that throws.
- Nested JSON objects become nested structs, as above.
- Keys that may be missing become optional properties, such as
let note: String?. - Dates need a decoder setting, such as
decoder.dateDecodingStrategy = .iso8601. - For snake_case keys such as
first_name, you can setdecoder.keyDecodingStrategy = .convertFromSnakeCaseinstead of writing CodingKeys by hand. Names with digits, liketemperature_2m, are better with explicitCodingKeys.
Calling the API with URLSession
URLSession is Apple's tool for network requests. Since Swift Concurrency arrived, the call is a single await. We use Open-Meteo, a weather service that was free to use for non-commercial projects without a key when this lesson was written. Check its current terms on its website before you use it in anything you publish.
enum APIError: Error {
case badURL
case badStatus(Int)
}
struct WeatherService {
func fetch(latitude: Double, longitude: Double) async throws -> Weather {
guard var parts = URLComponents(string: "https://api.open-meteo.com/v1/forecast") else {
throw APIError.badURL
}
parts.queryItems = [
URLQueryItem(name: "latitude", value: String(latitude)),
URLQueryItem(name: "longitude", value: String(longitude)),
URLQueryItem(name: "current", value: "temperature_2m,wind_speed_10m")
]
guard let url = parts.url else { throw APIError.badURL }
let (data, response) = try await URLSession.shared.data(from: url)
if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) {
throw APIError.badStatus(http.statusCode)
}
return try JSONDecoder().decode(Weather.self, from: data)
}
}
URLComponentsbuilds a web address safely, including the question mark and the encoding of special characters. It is better than gluing strings together.URLSession.shared.data(from:)isasync throws. It returns the bytes and a response object.- A server can answer with a failure code, for example 404 or 500, and
data(from:)does not throw for it. You must check the status code yourself. Codes from 200 to 299 mean success. - Everything that can go wrong, no internet, a bad status, unexpected JSON, ends up as a thrown error that the screen can catch.
Showing loading, result and error
A network call has three outcomes the person needs to see: waiting, success and failure. An enum from lesson 4 models this nicely. The screen is then a simple switch.
enum LoadState {
case idle
case loading
case loaded(Weather)
case failed(String)
}
struct WeatherView: View {
@State private var state: LoadState = .idle
private let service = WeatherService()
var body: some View {
VStack(spacing: 16) {
switch state {
case .idle, .loading:
ProgressView("Loading weather...")
case .loaded(let weather):
Text("\(weather.current.temperature, specifier: "%.1f") degrees")
.font(.largeTitle)
Text("Wind \(weather.current.windSpeed, specifier: "%.1f")")
.foregroundStyle(.secondary)
case .failed(let message):
Text(message).foregroundStyle(.red)
Button("Try again") { Task { await load() } }
}
}
.padding()
.task { await load() }
}
private func load() async {
state = .loading
do {
let weather = try await service.fetch(latitude: 52.52, longitude: 13.41)
state = .loaded(weather)
} catch {
state = .failed("Could not load the weather. Check your connection.")
}
}
}
The numbers 52.52 and 13.41 are the position of a city. Later you can ask for the user's location with Core Location, or let them type a city. The message in the failed case is written for a person. Do not show raw error text to users. It means nothing to them, and it can reveal things you would rather keep private.
Rules for real apps
- Use HTTPS. iOS blocks plain
httpaddresses by default for good reasons. - Never put a secret key in the app. Anything inside an app can be extracted. If an API needs a private key, such as a language model service, call your own server, which keeps the key and talks to the service for you. This is the pattern behind the AI courses on this site.
- Be ready for failure. Phones lose signal, tunnels exist. Always have an error state and a retry.
- Do not call on every key press. For search boxes, wait until the user stops typing for a moment.
- Respect the service's terms and rate limits. Free services often have limits.
Common mistakes
- Assuming a 404 or 500 throws an error. You have to check the status code.
- A struct that does not match the JSON, which gives a decoding error. Read the real response first.
- Updating the screen from a background thread. In SwiftUI with
@Stateand.task, you are on the main actor, which is what you want.
Your task
- Decode the sample JSON in a Playground and print the temperature.
- Open the Open-Meteo address in a browser, compare the real field names with your struct, and fix
CodingKeysif needed. - Build
WeatherViewand run it. Turn on Airplane Mode in the Simulator's host settings or use a wrong address to see the error state and the retry button. - Add a
TextFieldfor a city and one more field, such as a time or a unit, decoded from the response.
How to check it worked
- The screen shows a spinner, then a temperature, and a retry button when the call fails.
- You can say why a 404 does not throw by itself.
- You can say why a secret API key must never be inside the app.