URLSession and Codable: Call a REST API from SwiftUI

iOS App Development with Swift and SwiftUI: Beginner to Job-Ready Projects · Data and Networking · 8 min read · Updated

A network call

  1. The screen starts a task and shows a spinner while it waits.
  2. URLSession sends an HTTPS request to the web address.
  3. The server answers with a status code and JSON. You must check the status yourself.
  4. JSONDecoder turns the JSON into your Codable struct, and the screen shows the result or an error.

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 set decoder.keyDecodingStrategy = .convertFromSnakeCase instead of writing CodingKeys by hand. Names with digits, like temperature_2m, are better with explicit CodingKeys.

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)
    }
}
  • URLComponents builds 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:) is async 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 http addresses 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 @State and .task, you are on the main actor, which is what you want.

Your task

  1. Decode the sample JSON in a Playground and print the temperature.
  2. Open the Open-Meteo address in a browser, compare the real field names with your struct, and fix CodingKeys if needed.
  3. Build WeatherView and 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.
  4. Add a TextField for 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.

Frequently asked questions

What is Codable in Swift?

A protocol that lets a type be turned into and read from formats such as JSON. If property names match the keys, you write almost no code. CodingKeys handle differences.

Does URLSession throw an error for a 404 response?

No. It throws for problems such as no connection, but a 404 or 500 comes back as a normal response. Check the status code and throw your own error.

Is it safe to put an API key inside an iOS app?

No. Anything in an app can be extracted. Keep private keys on your own server and let the app call that server.