Day 22 – Fetching Data from an API (Networking with async/await)
T
Tuan Beo

Day 22 – Fetching Data from an API (Networking with async/await)

Learn how to make GET requests to a REST API using URLSession, decode JSON into Swift models with Codable, and display the data in a SwiftUI list.

Part 1 – Theory

1. What is networking in an iOS app?

Networking is how your app communicates with external services over the internet — for example:

  • Fetching data from APIs (weather, news, etc.)

  • Sending user data to a server (login, form submissions)

  • Uploading files or images

In iOS, networking happens over HTTP/HTTPS, usually in JSON format.


2. The steps in making an API request

  1. Create the URL of the resource you want

  2. Send the request using a networking tool

  3. Receive the response from the server

  4. Decode the response into Swift objects

  5. Update the UI to reflect the new data


3. The key players in Swift networking

  • URL → Represents the address of the resource

  • URLSession → Apple’s built-in networking API for making requests

  • JSONDecoder → Converts JSON data into Swift struct or class

  • Codable → Protocol that lets a type be encoded/decoded easily

  • Concurrency (async/await) → Introduced in Swift 5.5 (iOS 15+) for cleaner async code


4. Understanding HTTP requests & responses

Every request has:

  • URL (e.g., https://api.example.com/posts)

  • HTTP method: GET, POST, PUT, DELETE
    (GET = retrieve data; POST = send data)

  • Headers (metadata like Content-Type: application/json)

  • Body (for POST/PUT requests — the actual data you send)

Every response has:

  • Status code (200 = success, 404 = not found, 500 = server error)

  • Headers (metadata from server)

  • Body (the actual data — often JSON)


5. Why async/await is better than old callbacks

Before async/await:

URLSession.shared.dataTask(with: url) { data, response, error in
    // nested callbacks → harder to read
}

With async/await:

let (data, response) = try await URLSession.shared.data(from: url)

It’s:

  • Easier to read

  • Less nesting

  • Looks like synchronous code but still runs asynchronously


6. The Codable protocol

If your API returns JSON like:

{ "id": 1, "title": "Hello" }

You can decode it into:

struct Post: Codable {
    let id: Int
    let title: String
}

Swift automatically maps matching JSON keys to property names.


7. API request lifecycle in SwiftUI

  1. View appears (.task modifier runs)

  2. ViewModel calls API

  3. URLSession fetches data

  4. JSONDecoder parses it

  5. Published property updates → SwiftUI re-renders view


Part 2 – Implementation

We’ll use https://jsonplaceholder.typicode.com/posts — a free JSON API.

Step 1 – Model

struct Post: Identifiable, Codable {
    let userId: Int
    let id: Int
    let title: String
    let body: String
}
  • Identifiable → Makes it easy to use in List

  • Codable → Lets us decode JSON automatically


Step 2 – ViewModel

@MainActor
final class PostsViewModel: ObservableObject {
    @Published var posts: [Post] = []
    @Published var isLoading = false
    @Published var errorMessage: String?

    func fetchPosts() async {
        isLoading = true
        defer { isLoading = false } // always runs at function end

        guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else {
            errorMessage = "Invalid URL"
            return
        }

        do {
            let (data, response) = try await URLSession.shared.data(from: url)

            // Optional: Check status code
            if let httpResponse = response as? HTTPURLResponse,
               httpResponse.statusCode != 200 {
                throw URLError(.badServerResponse)
            }

            posts = try JSONDecoder().decode([Post].self, from: data)
        } catch {
            errorMessage = "Failed to fetch posts: \(error.localizedDescription)"
        }
    }
}

Key points:

  • @MainActor ensures updates happen on the main UI thread

  • @Published properties trigger SwiftUI UI updates

  • defer runs cleanup code when function exits

  • try await is used because both decoding and networking can fail


Step 3 – View

struct ContentView: View {
    @StateObject private var vm = PostsViewModel()

    var body: some View {
        NavigationStack {
            Group {
                if vm.isLoading {
                    ProgressView("Loading...")
                } else if let error = vm.errorMessage {
                    VStack(spacing: 12) {
                        Text(error)
                            .foregroundStyle(.red)
                        Button("Retry") {
                            Task { await vm.fetchPosts() }
                        }
                    }
                } else {
                    List(vm.posts) { post in
                        VStack(alignment: .leading, spacing: 4) {
                            Text(post.title)
                                .font(.headline)
                            Text(post.body)
                                .font(.subheadline)
                                .foregroundStyle(.secondary)
                        }
                        .padding(.vertical, 4)
                    }
                }
            }
            .navigationTitle("Posts")
            .task { // runs once when view appears
                await vm.fetchPosts()
            }
            .refreshable { // pull-to-refresh
                await vm.fetchPosts()
            }
        }
    }
}

Part 3 – Things to remember

  • Don’t block the main thread → Always use async networking

  • Handle all errors → network down, bad JSON, invalid URL

  • Check HTTP status codes → 200 is success, 400–500 means error

  • Model matches JSON keys → otherwise, use CodingKeys to map them

  • Security → Always use HTTPS for production apps


🛠 Practice

  1. Change API to https://jsonplaceholder.typicode.com/users and show usernames + emails.

  2. Add @Published var isRefreshing to differentiate between initial load & refresh.

  3. Display the number of posts in the navigation bar title.

  4. Show an empty state message if posts is empty.

Comments