Swift Concurrency -Manage API Calls in SwiftUI

Managing API calls efficiently in SwiftUI is crucial for building responsive and user-friendly apps. By using a network service class and Swift concurrency with async/await, you can handle API requests smoothly. This guide will show you how to structure your code to manage API calls, handle errors, and display appropriate messages to users.

Step 1: Define the Data Model

First, define the data model that represents the expense data.

import Foundation

struct Expense: Codable, Identifiable {
    var id: String
    var name: String
    var amount: Double
    var date: Date
    var category: String
    var payer: String
    var participants: [String]
}

Step 2: Create a Network Service

Next, create a network service class that handles API requests using URLSession and async/await. This class also monitors the network connection status.

import Foundation
import Network

class NetworkService {
    private let monitor = NWPathMonitor()
    private let queue = DispatchQueue(label: "NetworkMonitor")
    private var isConnected: Bool = true

    init() {
        monitor.pathUpdateHandler = { path in
            self.isConnected = path.status == .satisfied
        }
        monitor.start(queue: queue)
    }

    func fetchExpenses() async throws -> [Expense] {
        guard isConnected else {
            throw NetworkError.noInternetConnection
        }

        let url = URL(string: "https://api.example.com/expenses")!

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

        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw NetworkError.apiError
        }

        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        return try decoder.decode([Expense].self, from: data)
    }
}

enum NetworkError: Error, LocalizedError {
    case noInternetConnection
    case apiError

    var errorDescription: String? {
        switch self {
        case .noInternetConnection:
            return "No internet connection. Please check your network settings."
        case .apiError:
            return "Failed to fetch data from the server. Please try again later."
        }
    }
}

Step 3: Create the ViewModel

Create a ViewModel to manage the state and handle the API call using the network service.

import Foundation

class ExpenseViewModel: ObservableObject {
    @Published var expenses: [Expense] = []
    @Published var errorMessage: String?

    private let networkService = NetworkService()

    func loadExpenses() {
        Task {
            do {
                let fetchedExpenses = try await networkService.fetchExpenses()
                DispatchQueue.main.async {
                    self.expenses = fetchedExpenses
                }
            } catch {
                DispatchQueue.main.async {
                    self.errorMessage = error.localizedDescription
                }
            }
        }
    }
}

Step 4: Create the SwiftUI View

Create the SwiftUI view to display the expenses and show error messages if any.

import SwiftUI

struct ContentView: View {
    @StateObject var viewModel = ExpenseViewModel()

    var body: some View {
        NavigationView {
            VStack {
                if let errorMessage = viewModel.errorMessage {
                    Text(errorMessage)
                        .foregroundColor(.red)
                        .padding()
                }

                List(viewModel.expenses) { expense in
                    VStack(alignment: .leading) {
                        Text(expense.name)
                            .font(.headline)
                        Text("Amount: \(expense.amount)")
                        Text("Category: \(expense.category)")
                        Text("Payer: \(expense.payer)")
                        Text("Participants: \(expense.participants.joined(separator: ", "))")
                    }
                }
                .navigationTitle("Expenses")
                .onAppear {
                    viewModel.loadExpenses()
                }
            }
        }
    }
}

#Preview {
    ContentView()
}

Final Step: Handle No Internet Connection

The NetworkService already monitors the network connection status and throws an appropriate error if there’s no internet connection. The ViewModel handles this error and updates the errorMessage property, which the view then displays.

By following this approach, you ensure a clean separation of concerns with the network service handling the API calls, the ViewModel managing the state, and the SwiftUI view displaying the data and errors. This setup makes your code more maintainable and easier to understand.

Note: The API URL used in the fetchExpenses function (https://api.example.com/expenses) is a placeholder. You need to replace it with a valid API endpoint. If the provided URL is not reachable or invalid, it will result in an error such as “hostname could not be found.” Make sure to use a working API endpoint for the actual implementation.