Swift Combine Series: Part 2 — Networking with Combine
Welcome to Part 2 of our Swift Combine journey! If Part 1 got your Combine engines revving, this part will take you on a scenic drive through the networking highways. Grab your coffee (or tea, no judgment), and let’s unravel how Combine simplifies the art of fetching data and making API calls in Swift.
“Reactive programming isn’t magic — it’s just beautifully orchestrated logic.” Combine makes this orchestration feel like a symphony.
Why Combine for Networking?
Networking with Combine is like swapping a rusty old bicycle for a shiny new car. It’s sleek, modern, and gets the job done with style. Forget the callback pyramid of doom or juggling DispatchQueues — Combine handles asynchronicity like a pro.
- Declarative Syntax: Say goodbye to tangled code; write what you mean.
- Error Handling: Easily manage those pesky errors.
- Chaining: Transform and filter your data streams with elegance.
Making Your First Network Request
Let’s fetch some JSON data! In this example, we’ll use a public API to retrieve a list of posts.
Example 1: Fetching Posts
import Combine
import Foundation
// Define the model
struct Post: Decodable {
let id: Int
let title: String
let body: String
}
// Create a URL
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
// Create a publisher
let cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data } // Extract data
.decode(type: [Post].self, decoder: JSONDecoder()) // Decode JSON
.receive(on: DispatchQueue.main) // Update UI on the main thread
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Fetch completed successfully!")
case .failure(let error):
print("Error fetching posts: \(error)")
}
}, receiveValue: { posts in
print("Received posts: \(posts)")
})
Note: The
cancellable
instance must be stored outside the function body, such as in a property or an array, to prevent the subscription from being canceled prematurely. If it goes out of scope, the Combine framework will terminate the subscription before it completes its task.
Output:
Fetch completed successfully!
Received posts: [Post(id: 1, title: "Sample", body: "..."), ...]
Error Handling Like a Pro
Let’s see how Combine lets you handle errors gracefully.
Example 2: Handling Errors
let errorHandlingCancellable = URLSession.shared.dataTaskPublisher(for: url)
.tryMap { result -> Data in
guard let response = result.response as? HTTPURLResponse, response.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return result.data
}
.decode(type: [Post].self, decoder: JSONDecoder())
.catch { error in
Just([Post(id: 0, title: "Error", body: "Could not fetch data")]) // Fallback data
}
.sink(receiveValue: { posts in
print("Posts: \(posts)")
})
Transforming Data Streams
Combine shines when it comes to transforming data. Let’s see an example of fetching posts and transforming the titles to uppercase.
Example 3: Transforming Titles
let transformingCancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [Post].self, decoder: JSONDecoder())
.map { posts in
posts.map { $0.title.uppercased() }
}
.sink(receiveCompletion: { _ in }, receiveValue: { titles in
print("Transformed Titles: \(titles)")
})
Output:
Transformed Titles: ["LOREM IPSUM", "DOLOR SIT AMET", ...]
Practical Tips
- Debugging Requests: Use the
.print()
operator to inspect your data stream:
URLSession.shared.dataTaskPublisher(for: url)
.print()
.sink { ... }
- Caching Responses: Combine beautifully integrates with URLCache for caching network responses.
- Chaining Requests: Need to make sequential API calls? Combine lets you chain publishers seamlessly.
What’s Next?
In the next part of this series, we’ll dive into Combine and SwiftUI, exploring how to bind data streams directly to your UI. If you think Combine is fun now, just wait until you see it in action with SwiftUI. Until then, happy coding!