Making API Calls in StarterAppKit

This guide explains how to perform API calls in StarterAppKit, leveraging the NetworkManager for asynchronous requests and Combine for reactive data handling. StarterAppKit also integrates Alamofire to simplify HTTP networking.


Overview

StarterAppKit includes a NetworkManager utility for managing API calls. It is designed to provide:

  • Integration with Alamofire: Simplifies HTTP requests and response handling.
  • Access Token Management: Automatically retrieves and appends valid tokens to requests.
  • Error Handling: Handles errors such as unauthorized access and network issues.
  • Reactive Programming: Uses Combine to handle responses reactively.

Key Components

1. NetworkManager

The NetworkManager handles all API calls and returns a Combine publisher, making it easy to integrate with reactive Swift code.

Example: me() API Call

The me() function is used to fetch the current user's profile data.

Code Breakdown:

func me() -> AnyPublisher<(Int, Data), Error> { return Future<(Int, Data), Error> { [weak self] promise in guard let self = self else { promise(.failure(NetworkError.unknown)) return } Task { do { let accessToken = try await self.getValidAccessToken() let headers: HTTPHeaders = ["Authorization": "Bearer \(accessToken)"] AF.request(ApiEndpoints.baseurl + ApiEndpoints.user, method: .get, headers: headers).response { [weak self] response in switch response.result { case .success(let data): guard let data = data else { return } if let statusCode = response.response?.statusCode { if statusCode == 401 { self?.starterAppManager.signOut() } else { promise(.success((statusCode, data))) } } else { promise(.failure(AFError.responseValidationFailed(reason: .dataFileNil))) } case .failure(let error): promise(.failure(error)) } } } catch { promise(.failure(error)) } } } .eraseToAnyPublisher() }

Key Features:

  • Uses Alamofire: Handles HTTP requests and responses efficiently.
  • Token Management: Automatically appends a valid Bearer token to the request header.
  • Status Code Validation: Reacts to specific status codes (e.g., 401 for unauthorized).
  • Error Management: Uses Combine's Future to handle success and error states.

2. ViewModel Integration

API calls from the NetworkManager are injected into your ViewModel using the Factory framework for dependency injection.

Example:

import Factory @Injected(\StarterAppContainer.networkManager) private var networkManager networkManager.me() .sink(receiveCompletion: { completion in switch completion { case .finished: print("Update completed successfully.") case .failure(let error): print("Failed to complete request: \(error.localizedDescription)") } }, receiveValue: { [weak self] statusCode, data in print(statusCode) self?.handleUserResponse(statusCode: statusCode, data: data) }) .store(in: &cancellables)

Code Breakdown:

  1. Dependency Injection: The networkManager is injected into the ViewModel using Factory.
  2. Combine Integration: The me() function returns a publisher, making it easy to handle the response using sink.
  3. Error Handling:
    • receiveCompletion handles errors or indicates when the operation is complete.
    • Prints error messages or status codes for debugging.
  4. Reactive Updates: The receiveValue closure updates the app state or UI in response to API results.

Practical Example: Fetching User Profile Data

Here’s how you can fetch and process user profile data in your ViewModel:

import Factory import SwiftUI final class ExampleViewModel: ObservableObject { @Injected(\StarterAppContainer.networkManager) private var networkManager private var cancellables = Set<AnyCancellable>() @Published var userName: String = "" @Published var userEmail: String = "" func fetchUserProfile() { networkManager.me() .sink(receiveCompletion: { completion in switch completion { case .finished: print("User profile fetch completed successfully.") case .failure(let error): print("Failed to fetch user profile: \(error.localizedDescription)") } }, receiveValue: { [weak self] statusCode, data in guard let self = self else { return } self.parseUserProfileData(data: data) }) .store(in: &cancellables) } private func parseUserProfileData(data: Data) { do { let response = try JSONDecoder().decode(APIResponseModel.self, from: data) userName = response.user.firstName userEmail = response.user.emailAddress } catch { print("Failed to decode user profile data: \(error.localizedDescription)") } } }

Best Practices

  1. Use Alamofire for Networking:

    • Leverage its robust capabilities for request handling, response parsing, and error management.
  2. Dependency Injection:

    • Use Factory to inject the NetworkManager for clean, modular, and testable code.
  3. Error Handling:

    • Handle common errors like token expiration (401 Unauthorized) gracefully.
    • Log errors and provide user-friendly messages.
  4. Combine Pipelines:

    • Use .sink to process responses and completion states.
    • Store cancellables in a Set<AnyCancellable> to manage memory efficiently.
  5. Model Decoding:

    • Decode API responses into Swift models (Codable) for easy integration into your app.