Quick Summary:
Responding to a radical shift in the technology marketplace, let’s dive into the realm of tech magic with the Generic API Client powered by the Combine Framework. This blog revolves around the synergy of generics and the Combine framework in Swift for building a flexible and efficient generic API client. We can see the power of generics in their ability to create reusable and type-safe code, exemplified in Swift’s standard library. On the contrary, combine the declarative framework and enhance handling asynchronous tasks.
Table of Contents
Swift’s Generics and Combine framework provides a robust solution for crafting adaptable and type-safe code, particularly in networking tasks. Integrating the generic API client within Swift and the Combine framework brings numerous benefits that elevate development efficiency and codebase flexibility. Before delving into the Generic API Client with Combine Framework, let’s revisit our understanding of Generics and the Swift Framework
Generics in Swift serves as a powerful tool for writing flexible and efficient code writing. It allows the creation of functions and types that seamlessly work with different kinds of data without causing issues. The standard library of Swift extensively uses Generics, especially in collections like Array and Dictionary. These collections can handle different types of data, like numbers and words.
Generics empower developers to write concise and maintainable code, facilitating the creation of smart and adaptable solutions without repetitive code. Whether dealing with lists, organizing information, or developing custom functions, Swift’s support for generics simplifies code creation, making it flexible and easy to comprehend.
The Combine Framework handles the asynchronous events and maintains application states throughout their lifecycle. Combine offers a streamlined way to handle asynchronous events and the sequence of the values. It presents a structured API, using the publishers to emit changes and subscribers to consume evolving values.
In the Combine framework, the publisher protocol defines the types delivering stream values over time, with the operators manipulating these values. The subscribers in a sequence of publishers take action on received elements, and the values are dispatched in response to giving the subscribers control over the event reception pace.
Combine simplifies the integration output from various publishers, enabling easy orchestration of interactions. For example, subscribing to a text field’s publisher for updates and using the text data to initiate URL requests. Combine enhances code readability and maintainability by consolidating event-processing logic, eliminating the need for complex techniques like nested closures. This results in cleaner, more understandable code.
Now that we understand the basics of “Generics” and the “Combine Framework”, let us jump onto our topic for building a “Generic API Client With Combine Framework” that embodies the essence of flexibility and efficiency in Swift.
Let’s begin by establishing a new enum to serve as a centralized repository for all the endpoints essential for our application interactions. This enum will encapsulate the various endpoints and incorporate additional metadata for leverage in the following steps.
enum HTTPMethods: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
}
protocol EndPointType {
var path: String { get }
var url: URL? { get }
var method: HTTPMethods { get }
var body: Encodable? { get }
var headers: [String: String]? { get }
}
enum ProductEndPoint {
case products
case getProduct(model: DataRequestModel)
case addProduct(model: AddUpdateProduct)
case searchProduct(model: SearchData)
case updateProduct(model: AddUpdateProduct)
case deleteProduct(model: DataRequestModel)
}
extension ProductEndPoint: EndPointType {
var path: String {
switch self {
case .products:
return "products"
case .addProduct:
return "products/add"
case .searchProduct:
return "products/search"
case .getProduct(let model), .deleteProduct(let model):
return "products/\(model.id)"
case .updateProduct(let model):
return "products/\(model.id ?? 0)"
}
}
var url: URL? {
return URL(string: "https://dummyjson.com/\(path)")
}
var method: HTTPMethods {
switch self {
case .addProduct:
return .post
case .updateProduct:
return .put
case .deleteProduct:
return .delete
case .products, .getProduct, .searchProduct:
return .get
}
}
var body: Encodable? {
switch self {
case .addProduct(let model), .updateProduct(let model):
return model
case .products, .getProduct, .deleteProduct:
return nil
case .searchProduct(model: let model):
return model
}
}
var headers: [String : String]? {
return [
"Content-Type": "application/json"
]
}
}
Here, we have created a few data models, which are being used in the above code snippet.
struct SearchData: Encodable {
let q: String
}
struct DataRequestModel {
let id: Int
}
Hire iPhone App Developers and leverage the power of Swift with Swift Development.
struct AllProducts: Codable {
let products: [Product]
let total, skip, limit: Int
}
struct Product: Codable {
let id: Int
var title, description: String
let price: Int?
let discountPercentage, rating: Double?
let stock: Int?
let brand, category, thumbnail: String?
let images: [String]?
}
struct Rate: Codable {
let rate: Double
let count: Int
}
Let’s move to the main network layer, ‘HttpUtility’.
enum DataError: Error {
case invalidResponse
case invalidURL
case invalidData
case network(Error?)
case decoding(Error?)
case unknown(Error?)
}
typealias ResultHandler = Future
final class HttpUtility {
static let shared = HttpUtility()
private var cancellables = Set()
func request(modelType: T.Type,
type: EndPointType) -> ResultHandler {
return ResultHandler { [weak self] promise in
guard let self = self, let url = type.url else {
return promise(.failure(.invalidURL))
}
let request = getRequest(type: type, url: url)
URLSession.shared.dataTaskPublisher(for: request)
.tryMap(responseDataHandler)
.decode(type: T.self, decoder: JSONDecoder())
.receive(on: RunLoop.main)
.sink { [weak self] (completion) in
guard let self else { return }
if case let .failure(error) = completion {
let error = errorHandler(error: error)
return promise(.failure(error))
}
} receiveValue: { return promise(.success($0)) }
.store(in: &self.cancellables)
}
}
func getRequest(type: EndPointType, url: URL) -> URLRequest {
var request = URLRequest(url: url)
request.httpMethod = type.method.rawValue
if let parameters = type.body, type.method == .get {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
components?.queryItems = parameters.convertToURLQueryItems()
request.url = components?.url
} else if let parameters = type.body {
request.httpBody = try? JSONEncoder().encode(parameters)
}
request.allHTTPHeaderFields = type.headers
return request
}
func responseDataHandler(data: Data, response: URLResponse) throws -> Data {
guard let response = response as? HTTPURLResponse,
200...299 ~= response.statusCode else {
throw DataError.invalidResponse
}
return data
}
func errorHandler(error: Error) -> DataError {
switch error {
case let decodingError as DecodingError:
return .decoding(decodingError)
case let definedError as DataError:
return definedError
default:
return .unknown(error)
}
}
}
Referring to the code snippet above from the ‘HttpUtility.swift’.
Further, we have created a method called ‘request’, which takes two parameters:
Return type ‘ResultHandler
In this function, we return Future publisher with a promise to return error or data.
With the ‘getRequest()’ method, we created ‘URLRequest’ with the required request parameters.
Then, we called the ‘dataTaskPublisher’ from ‘URLSession’, which is a publisher, so it will require a subscriber.
Here, we have used the following terms after ‘dataTaskPublisher’:
Next, we created a service manager protocol for mocking API in unit test cases.
protocol ServiceManagerProtocol {
func fetchProducts(type: EndPointType) -> Future
func deleteProduct(type: EndPointType) -> Future
func searchProducts(type: EndPointType) -> Future
}
final class ServiceManager: ServiceManagerProtocol {
private var httpUtility = HttpUtility()
func fetchProducts(type: EndPointType) -> Future {
return httpUtility.request(modelType: AllProducts.self, type: type)
}
func deleteProduct(type: EndPointType) -> Future {
return httpUtility.request(modelType: Product.self, type: type)
}
func searchProducts(type: EndPointType) -> Future {
return httpUtility.request(modelType: AllProducts.self, type: type)
}
}
Moving ahead, let us see how we can call the API from ‘viewModel’.
final class ProductViewModel {
private var cancellables = Set()
init(serviceManager: ServiceManagerProtocol) {
self.serviceManager = serviceManager
}
func fetchProducts() {
serviceManager?.fetchProducts(type: ProductEndPoint.products)
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("Error \(error)")
case .finished:
print("Finished")
}
}, receiveValue: { allProducts in
print("All products \(allProducts)")
})
.store(in: &cancellables)
}
func deleteProduct(model: DataRequestModel) {
serviceManager?.deleteProduct(type: ProductEndPoint.deleteProduct(model: model))
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("Error \(error)")
case .finished:
print("Finished")
}
}, receiveValue: { _ in
print("Product deleted")
})
.store(in: &cancellables)
}
func searchProducts(model: SearchData) {
serviceManager?.searchProducts(type: ProductEndPoint.searchProduct(model: model))
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("Error \(error)")
case .finished:
print("Finished")
}
}, receiveValue: { allProducts in
print("All products \(allProducts)")
})
.store(in: &cancellables)
}
}
Here, we have created a dependency injection for the service manager protocol, and from that instance, we are calling the API. As the API method request returns the Future Publisher, we have used the sink subscriber to get the error if there is any; otherwise, it will return data. Also, we have used the store to retain the subscription in memory. We have separated concerns by centralizing the networking concerns within the HttpUtility.
In the eventuality of needing to incorporate additional endpoints in the future, the process is streamlined and straightforward. All that is required is the definition of the new endpoint. We seamlessly integrate the new endpoint into our API system by specifying its pertinent information, including the path, method, and associated parameters.
Summing up the information, the Generic API Client With Combine Framework mentioned within this blog post seamlessly integrates Swift’s generics and Combine framework to offer a versatile and effective networking solution. The systematic approach to constructing the Generic API Client centralizing concerns in ‘HttpUtility’, and leveraging the Combine’s Future Publisher results in a clean, maintainable, and adaptable solution. The design simplifies the addition of new endpoints, aligning with the current technological trends and providing the developers with a powerful tool for handling asynchronous events in Swift applications. However, if you are a business owner, you can Hire Dedicated Developers from Bacancy to help you with your application development process.
The Generic API Client is a versatile tool that utilizes Swift’s generics to create reusable and type-safe code for interacting with APIs. It facilitates the handling of various endpoint types and responses.
Adding new endpoints is straightforward. Simply define a new class with the appropriate enum (e.g., ProductEndPoint) and implement the associated properties like path, method, body, etc. This follows a systematic approach for easy integration.
Combine’s Future Publisher provides a clear and concise way to handle asynchronous responses. It simplifies error handling and allows developers to respond to API results reactively and efficiently.
Yes, the design principles of the Generic API Client, such as separation of concerns and code organization, make it suitable for large-scale applications. Its flexibility and scalability accommodate the evolving needs of extensive projects.
Your Success Is Guaranteed !
We accelerate the release of digital product and guaranteed their success
We Use Slack, Jira & GitHub for Accurate Deployment and Effective Communication.