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

Introduction

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

What is Generics?

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.

What is the Combine?

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.

Steps For Building Generic API Client

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.

EndPointTypes.swift

Copy Text
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 }
}

ProductEndPoint.swift

Copy Text
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.

SearchData.swift

Copy Text
struct SearchData: Encodable {
    let q: String
}

DataRequestModel.swift

Copy Text
struct DataRequestModel {
    let id: Int
}
Want Your iOS Application To Stand Out?

Hire iPhone App Developers and leverage the power of Swift with Swift Development.

AllProducts.swift

Copy Text
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’.

HttpUtility.swift

Copy Text
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’.

  • Here, we have created a ‘DataError’ for defining the custom errors.
  • Then, we have defined a type alias that matches the ‘URLSession’s’ method response to add more semantics to the code.

Further, we have created a method called ‘request’, which takes two parameters:

  • modelType (i.e., response type, which we are expecting from API)
  • type (i.e. endpoint type, which defines the path, body, and all the required parameters to call the API)

Return type ‘ResultHandler, the type alias of the Future Publisher.

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’:

  • tryMap: it will get data from upstream and do some process on it, i.e., it will check the ‘HTTPURLResponse’ status code, which should be between 200-299; to check this, we have created a separate method called ‘responseDataHandler’.
  • decode: It will decode the response data into a proper response model.
  • receive: It will define if an API call will happen on the main or background thread.
  • sink: This is a subscriber of the publisher ‘dataTaskPublisher’; it has two blocks, one for receive completion and one for receive value. In receive completion, it will give errors if there are any, or otherwise ‘finished’ state it will give. We will get data in the receipt value block if everything is fine.
  • store: This will store ‘AnyCancellable’ set to cancel the subscription when reinitialized.

Next, we created a service manager protocol for mocking API in unit test cases.

ServiceManager.swift

Copy Text
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’.

ProductViewModel.swift

Copy Text
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.

Conclusion

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.

Frequently Asked Questions (FAQs)

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.

Get The Most Out Of Your iOS Apps With Swift.

Contact Us!

Build Your Agile Team

Hire Skilled Developer From Us

[email protected]

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.

How Can We Help You?