How to fetch remote data using Combine Framework in SwiftUI

Search for a command to run...

No comments yet. Be the first to comment.
Some mobile flows are not APIs. They are negotiations with the operating system. USSD is one of those flows.

Reflecting on My Birthday
How Coding Knowledge Came to the Rescue in a Kitchen Crisis

📷 Photo by Behnam Norouzi Introduction When working on a recent client project, I encountered a serious performance issue: the app was consuming excessive memory, leading to slow performance, crashes, and issues with lists rendering large images. ...

As an iOS engineer with a passion for leveraging technology to address real-world challenges, I had the privilege of serving as a mentor and judge at CodeXtreme 2025. This year's hackathon, themed "Build Things People Need: High-Value, High-Impact Te...

During WWDC 2019, Apple introduces Combine which provides a declarative Swift API for processing values over time.
These values can represent many kinds of asynchronous events. Combine declares publishers to expose values that can change over time, and subscribers to receive those values from the publishers.
Today, we will focus on how we can utilize combine's publishers and subscribers system to fetch remote data from a remote data source and populate the data to our UI.
We'll keep it simple and stupid!; I chose the Random Data API, and we'll be using their Users Endpoint which provides some mock information for a random user.
We are going to need the RemoteUser and the Address models
// MARK: - RemoteUser
struct RemoteUser: Identifiable, Codable {
let id: Int
let firstName, lastName: String
let username, email: String
let phoneNumber: String
let address: Address
enum CodingKeys: String, CodingKey {
case id
case firstName = "first_name"
case lastName = "last_name"
case username, email
case phoneNumber = "phone_number"
case address
}
}
// MARK: - Address
struct Address: Codable {
let city: String
let state: String
let country: String
enum CodingKeys: String, CodingKey {
case city
case state, country
}
func formatted() -> String {
return "\(city), \(state), \(country)"
}
}
fetch functionThis function does the heavy lifting for us, taking an input of type URLRequest as a parameter and returning an output of type AnyPublisher<T, Error>
func perform<T: Decodable>(_ request: URLRequest) -> AnyPublisher<T, Error> {
return URLSession.shared
.dataTaskPublisher(for: request) // Returns a publisher that wraps a URL session data task for a given URL request.
.map { $0.data }
.decode(type: T.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main) // Receive elements on the Main Queue
.eraseToAnyPublisher()
}
Basically, in our ContenView, we will use a simple List where we shall be showing our user information represented by a UserRowView
import Combine
struct ContentView: View {
@State private var users = [RemoteUser]() // Store our users
@State private var isFetchingData = false // Manage the fetching spinner
@State private var cancellables = Set<AnyCancellable>()
var body: some View {
NavigationView {
List {
ForEach(users) { user in
UserRowView(user)
}
if isFetchingData {
ProgressView()
.frame(maxWidth: .infinity)
}
}
.navigationTitle("\(users.count) Active Users")
.onAppear(perform: fetchSomeData) // fetch data when view appears
}
}
// Some raw URLRequest for testing purpose
private func getURLRequest() -> URLRequest {
var request = URLRequest(url: URL(string: "https://random-data-api.com/api/v2/users")!)
request.httpMethod = "GET"
return request
}
// Perform network call
private func fetchSomeData() {
guard users.count < 50 else { return }
let result: AnyPublisher<RemoteUser, Error> = perform(getURLRequest())
isFetchingData = true
result.sink { _ in
} receiveValue: { item in
print(item)
self.users.append(item)
isFetchingData = false
fetchSomeData() // Fetch new data when request has completed
}
.store(in: &cancellables) // Stores a type-erasing cancellable instance.
}
}
struct UserRowView: View {
private let user: RemoteUser
init(_ user: RemoteUser) {
self.user = user
}
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text("Name: \(user.firstName) \(user.lastName)")
.fontWeight(.bold)
Text("Username: \(Text("@\(user.username)").foregroundColor(.accentColor))")
.fontWeight(.semibold)
Text("Tél: \(user.phoneNumber)")
.fontWeight(.medium)
Text("Address: \(user.address.formatted())")
.italic()
.foregroundColor(.secondary)
}
.padding(8)
.frame(maxWidth: .infinity, alignment: .leading)
.cornerRadius(10)
}
}
You will observe that on every successful request, another request will be made until the number of users reaches 50 as specified in the code.

Combine framework has a lot to offer, but in this article, we have just scratched the tip of the iceberg, I hope you have learned something today :)