Welcome to our tutorial on using the Combine framework to efficiently sync fetching data from multiple sources in SwiftUI! In this tutorial, we will explore how to use Combine to load data from different sources, such as APIs, and combine them into a cohesive whole. We will also show how to provide a completion callback when all data has been loaded, allowing you to track the progress of your data loading and take appropriate actions.
By the end of this tutorial, you will have a good understanding how to use Combine to load data from multiple sources in your SwiftUI applications.
Getting started with the Combine framework for data loading in SwiftUI
In the context of SwiftUI, the Combine framework can be used to load data from a variety of sources and provide a completion callback when all data has been loaded. This ensures all data is available before rendering the UI. Using Combine for data loading has several advantages, such as managing multiple streams of data, handling errors and cancellations, and providing a completion callback.
Data loading is an important aspect of building efficient and responsive SwiftUI applications. By using the Combine framework, we can avoid common pitfalls such as blocking the main thread or overloading the network. Instead, we can focus on building robust and performant user interfaces.
Welcome to our tutorial on using the Combine framework to efficiently sync fetching data from multiple sources in SwiftUI! In this tutorial, we will explore how to use Combine to load data from different sources, such as APIs, and combine them into a cohesive whole. We will also show how to provide a completion callback when all data has been loaded, allowing you to track the progress of your data loading and take appropriate actions.
By the end of this tutorial, you will have a good understanding how to use Combine to load data from multiple sources in your SwiftUI applications.
Getting started with the Combine framework for data loading in SwiftUI
In the context of SwiftUI, the Combine framework can be used to load data from a variety of sources and provide a completion callback when all data has been loaded. This ensures all data is available before rendering the UI. Using Combine for data loading has several advantages, such as managing multiple streams of data, handling errors and cancellations, and providing a completion callback.
Data loading is an important aspect of building efficient and responsive SwiftUI applications. By using the Combine framework, we can avoid common pitfalls such as blocking the main thread or overloading the network. Instead, we can focus on building robust and performant user interfaces.
Welcome to our tutorial on using the Combine framework to efficiently sync fetching data from multiple sources in SwiftUI! In this tutorial, we will explore how to use Combine to load data from different sources, such as APIs, and combine them into a cohesive whole. We will also show how to provide a completion callback when all data has been loaded, allowing you to track the progress of your data loading and take appropriate actions.
By the end of this tutorial, you will have a good understanding how to use Combine to load data from multiple sources in your SwiftUI applications.
Getting started with the Combine framework for data loading in SwiftUI
In the context of SwiftUI, the Combine framework can be used to load data from a variety of sources and provide a completion callback when all data has been loaded. This ensures all data is available before rendering the UI. Using Combine for data loading has several advantages, such as managing multiple streams of data, handling errors and cancellations, and providing a completion callback.
Data loading is an important aspect of building efficient and responsive SwiftUI applications. By using the Combine framework, we can avoid common pitfalls such as blocking the main thread or overloading the network. Instead, we can focus on building robust and performant user interfaces.
Loading data from multiple sources is a common task for developers. While Combine provides an efficient way to manage asynchronous data streams, it also presents some challenges. One of the biggest challenges is ensuring that data is loaded correctly and efficiently.
A common mistake is loading one batch of data, waiting for it to be loaded, and then moving on to the next data source. This approach can be time-consuming, especially if the data sources are related to each other and require additional calculations. Additionally, errors during the data loading process can cause the entire operation to crash, leaving the user with incomplete data.
To avoid these issues, developers must ensure that they are properly synchronizing data fetching from multiple sources. They should also consider error handling and implementing a backup plan in case of data loading failures.
Setting up a basic data loading pipeline
This example demonstrates how to set up a basic data loading pipeline using the Combine framework in a SwiftUI application. The data we will work with is a list of cryptocurrency prices retrieved from a publicly available API.
To start, we define a view model class that handles the data loading and management. This class uses the URLSession
publisher to create a publisher that emits data from a network request to the API. We then subscribe to the publisher using the .sink
operator and provide a closure that updates the cryptoPrices
property with the received data and sets the loadingCompleted
flag to true
to indicate that the data has been loaded. To ensure that updates to the cryptoPrices
and loadingCompleted
properties occur on the main thread, we use the receive(on:)
operator.
import SwiftUI
import Combine
struct CryptoPrice: Decodable, Identifiable {
let id: String
let name: String
let symbol: String
let quotes: Quote
struct Quote: Decodable {
let USD: USD
struct USD: Decodable {
let price: Double
}
}
}
class CryptoViewModel: ObservableObject {
@Published var cryptoPrices: [CryptoPrice] = []
@Published var loadingCompleted = false
@Published var error: Error?
private var cancellables = Set<AnyCancellable>()
func fetchCryptoPrices() {
loadingCompleted = false
let url = URL(string: "https://api.coinpaprika.com/v1/tickers?limit=100")!
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [CryptoPrice].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
self.error = error
case .finished:
self.loadingCompleted = true
}
}, receiveValue: { self.cryptoPrices = $0 })
.store(in: &cancellables)
}
}
struct ContentView: View {
@ObservedObject var cryptoViewModel = CryptoViewModel()
var body: some View {
VStack {
if cryptoViewModel.error != nil {
Text("An error occurred: \(cryptoViewModel.error!.localizedDescription)")
} else if !cryptoViewModel.loadingCompleted {
VStack {
ProgressView()
.padding(8)
Text("Loading...")
}
} else {
List(cryptoViewModel.cryptoPrices) { cryptoPrice in
Text("\(cryptoPrice.name) (\(cryptoPrice.symbol)) - $\(cryptoPrice.quotes.USD.price)")
}
}
}.onAppear {
self.cryptoViewModel.fetchCryptoPrices()
}
}
}
In summary, the ContentView
struct in our SwiftUI application displays different content to the user based on the state of the data loading process. When the view appears, it calls the fetchCryptoPrices
method of the CryptoViewModel
instance to initiate the data loading process.
If an error occurs during the data loading process, an error message is displayed. If the data is still being loaded, a loading indicator is displayed. If the data has been successfully loaded, a list of cryptocurrency prices is displayed.
This approach enables the ContentView
view to show the appropriate content to the user as the data is being loaded and processed.
Fetch Data from Multiple Sources
Before diving into using Combine frameworks to load data from multiple sources as one stream, let's first take a look at how to retrieve data from multiple APIs in a simple way. In this example, we will be using two separate APIs to retrieve data: one for cryptocurrency prices and one for stock quotes.
import SwiftUI
import Combine
struct CryptoPrice: Decodable, Identifiable {
let id: String
let name: String
let symbol: String
let quotes: Quote
struct Quote: Decodable {
let USD: USD
struct USD: Decodable {
let price: Double
}
}
}
class CryptoViewModel: ObservableObject {
@Published var cryptoPrices: [CryptoPrice] = []
@Published var loadingCompleted = false
@Published var error: Error?
private var cancellables = Set<AnyCancellable>()
func fetchCryptoPrices() {
loadingCompleted = false
let url = URL(string: "https://api.coinpaprika.com/v1/tickers?limit=100")!
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [CryptoPrice].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
self.error = error
case .finished:
self.loadingCompleted = true
}
}, receiveValue: { self.cryptoPrices = $0 })
.store(in: &cancellables)
}
}
struct StockResponse: Decodable {
let quoteResponse: QuoteResponse
struct QuoteResponse: Decodable {
let result: [Stock]
}
}
struct Stock: Decodable, Identifiable {
let symbol: String
let longName: String
let regularMarketPrice: Double
var id: String { symbol }
}
class StockViewModel: ObservableObject {
@Published var stocks: [Stock] = []
@Published var loadingCompleted = false
@Published var error: Error?
private var cancellables = Set<AnyCancellable>()
func fetchStocks() {
loadingCompleted = false
let url = URL(string: "https://query1.finance.yahoo.com/v7/finance/quote?symbols=AAPL,GOOG,MSFT,TSLA,UBER,LYFT")!
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: StockResponse.self, decoder: JSONDecoder())
.map { $0.quoteResponse.result }
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
self.error = error
case .finished:
self.loadingCompleted = true
}
}, receiveValue: { self.stocks = $0 })
.store(in: &cancellables)
}
}
enum Selection {
case stocks
case crypto
}
struct ContentView: View {
@ObservedObject var stockViewModel = StockViewModel()
@ObservedObject var cryptoViewModel = CryptoViewModel()
@State private var selection: Selection = .stocks
var body: some View {
VStack {
if stockViewModel.error != nil || cryptoViewModel.error != nil {
Text("An error occurred while loading data from API")
} else if !stockViewModel.loadingCompleted || !cryptoViewModel.loadingCompleted {
VStack {
ProgressView()
.padding(8)
Text("Loading...")
}
} else {
Picker("Options", selection: $selection) {
Text("Stocks").tag(Selection.stocks)
Text("Crypto").tag(Selection.crypto)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
if selection == .crypto {
List(cryptoViewModel.cryptoPrices) { cryptoPrice in
Text("\(cryptoPrice.name) (\(cryptoPrice.symbol)) - $\(cryptoPrice.quotes.USD.price)")
}
} else {
List(stockViewModel.stocks) { stock in
Text("\(stock.symbol) - \(stock.longName) - $\(stock.regularMarketPrice)")
}
}
}
}
.onAppear {
self.stockViewModel.fetchStocks()
self.cryptoViewModel.fetchCryptoPrices()
}
}
}
In this example, we defined two view models: the CryptoViewModel
and the StockViewModel
. Each handles data loading and management for cryptocurrency prices and stock prices, respectively. Both view models use the URLSession
publisher to create a publisher that emits data from a network request to a publicly available API.
When the ContentView
view appears, it calls the fetchCryptoPrices
and fetchStocks
methods of the CryptoViewModel
and StockViewModel
instances, respectively. Both methods initiate the data loading process for their respective data sources.
However, this implementation has a limitation: we must check the loading status and error state for each data source separately. This can become cumbersome as the number of data sources increases. In the next example, we'll explore how the Combine framework can simplify this process.
Using Combine's zip operator to merge data from multiple sources
This example demonstrates how to use the Combine framework's zip
operator to retrieve data from two different sources (a cryptocurrency prices API and a stock quotes API) and combine them into a single stream of data. To do so, we first update our view model classes (CryptoViewModel
and StockViewModel
) to return a publisher of the data. Then, we create a new DataManager
class that uses the zip
operator to combine the data from the two view models into a single stream.
Once we have our data stream set up, we can use the sink operator to subscribe to it and receive updates whenever new data becomes available. In this case, we update the loadingCompleted
flag to indicate that the data has been loaded and update the data property with the received data.
import SwiftUI
import Combine
struct CryptoPrice: Decodable, Identifiable {
let id: String
let name: String
let symbol: String
let quotes: Quote
struct Quote: Decodable {
let USD: USD
struct USD: Decodable {
let price: Double
}
}
}
class CryptoViewModel: ObservableObject {
@Published var cryptoPrices: [CryptoPrice] = []
func fetchCryptoPrices() -> AnyPublisher<[CryptoPrice], Error> {
let url = URL(string: "https://api.coinpaprika.com/v1/tickers?limit=20")!
return Future<[CryptoPrice], Error> { promise in
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
promise(.failure(error))
} else if let data = data {
do {
let cryptoPrices = try JSONDecoder().decode([CryptoPrice].self, from: data)
self.cryptoPrices = cryptoPrices
promise(.success(cryptoPrices))
} catch {
promise(.failure(error))
}
}
}
task.resume()
}
.eraseToAnyPublisher()
}
}
struct StockResponse: Decodable {
let quoteResponse: QuoteResponse
struct QuoteResponse: Decodable {
let result: [Stock]
}
}
struct Stock: Decodable, Identifiable {
let symbol: String
let longName: String
let regularMarketPrice: Double
var id: String { symbol }
}
class StockViewModel: ObservableObject {
@Published var stocks: [Stock] = []
func fetchStocks() -> AnyPublisher<[Stock], Error> {
let url = URL(string: "https://query1.finance.yahoo.com/v7/finance/quote?symbols=AAPL,GOOG,MSFT,TSLA,UBER,LYFT")!
return Future<[Stock], Error> { promise in
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
promise(.failure(error))
} else if let data = data {
do {
let response = try JSONDecoder().decode(StockResponse.self, from: data)
self.stocks = response.quoteResponse.result
promise(.success(self.stocks))
} catch {
promise(.failure(error))
}
}
}
task.resume()
}
.eraseToAnyPublisher()
}
}
enum Selection {
case stocks
case crypto
}
class DataManager: ObservableObject {
@Published var stockViewModel = StockViewModel()
@Published var cryptoViewModel = CryptoViewModel()
private var cancellables = Set<AnyCancellable>()
@Published var loadingCompleted = false
@Published var error: Error?
func loadData() {
loadingCompleted = false
let stockPublisher = stockViewModel.fetchStocks()
let cryptoPublisher = cryptoViewModel.fetchCryptoPrices()
Publishers.Zip(stockPublisher, cryptoPublisher)
.receive(on: DispatchQueue.main)
.retry(3)
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
self.error = error
case .finished:
self.loadingCompleted = true
}
}, receiveValue: { stocks, crypto in
print("Stocks loaded:\(stocks.count)")
print("Cryptocurrencies loaded:\(crypto.count)")
})
.store(in: &cancellables)
}
}
struct ContentView: View {
@ObservedObject var dataManager = DataManager()
@State private var selection: Selection = .stocks
var body: some View {
VStack {
if dataManager.error != nil {
Text("An error occurred while loading data from API")
} else if !dataManager.loadingCompleted {
VStack {
ProgressView()
.padding(8)
Text("Loading...")
}
} else {
Picker("Options", selection: $selection) {
Text("Stocks").tag(Selection.stocks)
Text("Crypto").tag(Selection.crypto)
}
.pickerStyle(SegmentedPickerStyle())
.padding()
if selection == .crypto {
List(dataManager.cryptoViewModel.cryptoPrices) { cryptoPrice in
Text("\(cryptoPrice.name) (\(cryptoPrice.symbol)) - $\(cryptoPrice.quotes.USD.price)")
}
} else {
List(dataManager.stockViewModel.stocks) { stock in
Text("\(stock.symbol) - \(stock.longName) - $\(stock.regularMarketPrice)")
}
}
}
}
.onAppear {
self.dataManager.loadData()
}
}
}
Using the zip
operator enables us to handle the data loading process in a more streamlined way. We no longer have to check the loading status of each data source separately. This can prevent partial updates or race conditions that could occur if the data was loaded asynchronously and displayed as soon as it was available.
The retry(#)
modifier allows us to automatically retry the API request a couple times if it fails due to a network or server error. This can improve the reliability of the data loading process and reduce the likelihood of errors being displayed to the user.
Making your app faster and more efficient with caching and automatic data updates
If your database allows you to listen for changes, you can further improve your code by implementing listeners that detect changes in the data. Once the necessary data is loaded on start-up, you can continuously listen for changes and automatically update the data without requiring any manual action from the user. Caching the loaded data can also significantly improve app performance by eliminating the need to repeatedly fetch the same data, saving valuable time and resources.
This approach can greatly enhance the user experience by ensuring that the data is always up-to-date without the need for constant manual updates. Additionally, implementing listeners for automatic data updates and caching the loaded data can improve app performance overall.