
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
Create the URL of the resource you want
Send the request using a networking tool
Receive the response from the server
Decode the response into Swift objects
Update the UI to reflect the new data
3. The key players in Swift networking
URL→ Represents the address of the resourceURLSession→ Apple’s built-in networking API for making requestsJSONDecoder→ Converts JSON data into SwiftstructorclassCodable→ Protocol that lets a type be encoded/decoded easilyConcurrency (
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
View appears (
.taskmodifier runs)ViewModel calls API
URLSessionfetches dataJSONDecoderparses itPublished 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 inListCodable→ 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:
@MainActorensures updates happen on the main UI thread@Publishedproperties trigger SwiftUI UI updatesdeferruns cleanup code when function exitstry awaitis 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
CodingKeysto map themSecurity → Always use HTTPS for production apps
🛠 Practice
Change API to
https://jsonplaceholder.typicode.com/usersand show usernames + emails.Add
@Published var isRefreshingto differentiate between initial load & refresh.Display the number of posts in the navigation bar title.
Show an empty state message if
postsis empty.
