Wes Matlock

Creating a Simple Network Manager/Client in SwiftUI Using Protocols, async/await, and URLCache

When developing apps in SwiftUI, handling network requests efficiently is crucial for providing a seamless user experience. In this post…


Creating a Simple Network Manager/Client in SwiftUI Using Protocols, async/await, and URLCache

When developing apps in SwiftUI, handling network requests efficiently is crucial for providing a seamless user experience. In this post, we will create a simple network manager/client using SwiftUI, protocols, async/await, and URLCache to handle RESTful network calls. This approach will ensure clean code, separation of concerns, and easy testing.

Step 1: Define the API Protocol

First, define a protocol that outlines the basic functions your network manager will support. This will ensure that any network manager class conforms to a standard interface.

protocol APIClient {  
    func get<T: Decodable>(url: URL) async throws -> T  
    func post<T: Decodable, U: Encodable>(url: URL, body: U) async throws -> T  
}

Step 2: Create a Network Manager

Next, create a class that implements this protocol. This class will handle the actual network requests using URLSession, async/await, and URLCache.

import Foundation  
  
class NetworkManager: APIClient {  
    private let urlCache: URLCache  
      
    init(urlCache: URLCache = .shared) {  
        self.urlCache = urlCache  
    }  
      
    func get<T: Decodable>(url: URL) async throws -> T {  
        if let cachedResponse = urlCache.cachedResponse(for: URLRequest(url: url)) {  
            let decodedData = try JSONDecoder().decode(T.self, from: cachedResponse.data)  
            return decodedData  
        }  
          
        let (data, response) = try await URLSession.shared.data(from: url)  
          
        if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {  
            let cachedResponse = CachedURLResponse(response: response, data: data)  
            urlCache.storeCachedResponse(cachedResponse, for: URLRequest(url: url))  
        }  
          
        let decodedData = try JSONDecoder().decode(T.self, from: data)  
        return decodedData  
    }  
      
    func post<T: Decodable, U: Encodable>(url: URL, body: U) async throws -> T {  
        var request = URLRequest(url: url)  
        request.httpMethod = "POST"  
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")  
        request.httpBody = try JSONEncoder().encode(body)  
          
        let (data, _) = try await URLSession.shared.data(for: request)  
        let decodedData = try JSONDecoder().decode(T.self, from: data)  
        return decodedData  
    }  
}

Step 3: Create a ViewModel

Now, create a ViewModel that will use the network manager to fetch data and update the view. This ViewModel will conform to ObservableObject to enable SwiftUI to react to data changes.

import SwiftUI  
import Combine  
  
class ExampleViewModel: ObservableObject {  
  @Published var data: [SampleData] = []  
  private let apiClient: APIClient  
  
  init(apiClient: APIClient = NetworkManager()) {  
    self.apiClient = apiClient  
  }  
  
  func fetchData() async {  
    guard let url = URL(string: "https://api.example.com/data") else { return }  
  
    do {  
      let fetchedData: [SampleData] = try await apiClient.get(url: url)  
      DispatchQueue.main.async {  
        self.data = fetchedData  
      }  
    } catch {  
      print("Error fetching data: \(error.localizedDescription)")  
    }  
  }  
}  
// MARK: - Sample Data  
struct SampleData: Decodable, Identifiable {  
  var id = UUID()  
  let name: String  
}

Step 4: Create the SwiftUI View

Finally, create a SwiftUI view that uses the ViewModel to display data. This view will use the @StateObject property wrapper to create and manage the ViewModel’s lifecycle.

import SwiftUI  
  
struct ExampleView: View {  
  @StateObject private var viewModel = ExampleViewModel()  
  
  var body: some View {  
    List(viewModel.data) { item in  
      Text(item.name) // Replace with appropriate property  
    }  
    .task {  
      await viewModel.fetchData()  
    }  
  }  
}

Conclusion

This approach ensures a clean separation of concerns and makes the network manager easily testable and reusable. You can further extend this setup by adding more HTTP methods (PUT, DELETE), handling different types of errors, and implementing retry mechanisms.

Additional Tips

Error Handling: Consider creating a custom error type to better handle different error scenarios.

Dependency Injection: Pass different implementations of APIClient to your ViewModel for testing purposes.

EnvironmentObject: Use EnvironmentObject to pass the network manager throughout your SwiftUI views if needed.

By following these steps, you can create a robust and reusable network manager in SwiftUI using protocols, async/await, and URLCache. This setup provides a solid foundation for handling network requests in a clean and maintainable way.

Happy coding! 🚀

By Wesley Matlock on May 28, 2024.

Canonical link

Exported from Medium on May 10, 2025.

Written on May 28, 2024