Using Combine 1

  1. API End point
//
//  APIEndpoint.swift
//  FutureAPIDemo
//
//  Created by Joynal Abedin on 3/11/23.
//

import Foundation

///APIEndpoint
protocol APIEndpoint {
    
    var baseURL: URL { get }
    var path: String { get }
    var method: HTTPMethod { get }
    var headers: [String: String]? { get }
    var parameters: [String: String]? { get }
}

///HTTPMethod
enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
    case put = "PUT"
    case patch = "PATCH"
    case delete = "DELETE"
}

//MARK: - URLEndpoint
enum URLEndpoint: APIEndpoint {
    
    ///all api` cases`
    case bookList
    case searchBook(name: String)
    
    ///ap `baseURL`
    var baseURL: URL {
        guard let url = URL(string: "https://devilarticle.com") else {
            fatalError()
        }
        return url
    }
    
    ///api `endPoint`
    var path: String {
        switch self {
        case .bookList:
            return "/api/LibraryService/GetAllBook"
        case .searchBook:
            return "/api/LibraryService/SearchBook"
        }
    }
    
    ///api `method`
    var method: HTTPMethod {
        switch self {
        case .bookList:
            return .get
        case .searchBook:
            return .get
        }
    }
    
    ///api `headers`
    var headers: [String: String]? {
        switch self {
        case .bookList:
            return ["Content-Type": "application/json"]
        case .searchBook(name: _):
            return ["Content-Type": "application/json"]
        }
    }
    
    ///api `parameters`
    var parameters: [String: String]? {
        switch self {
        case .bookList:
            return nil
        case .searchBook(name: let name):
            return ["name": name]
        }
    }
    
}

2. API Service

//
//  APIService.swift
//  CombineAPIDemo1
//
//  Created by Joynal Abedin on 4/11/23.
//

import Foundation
import Combine

///API Response errors
enum APIError: Error {
    case invalidResponse
    case invalidData
}

///protocol
protocol APIClient {
    associatedtype EndpointType: APIEndpoint
    func request<T: Decodable>(_ endpoint: EndpointType) -> AnyPublisher<T, Error>
}

//MARK: - APIService
class APIService<EndpointType: APIEndpoint>: APIClient {
    func request<T: Decodable>(_ endpoint: EndpointType) -> AnyPublisher<T, Error> {
        
        let baseAppend = endpoint.baseURL.appendingPathComponent(endpoint.path).absoluteString.removingPercentEncoding
        var request = URLRequest(url: URL(string: baseAppend!)!)
        
        ///added api endpoint
        request.httpMethod = endpoint.method.rawValue
        /// Set up any request `headers` or `parameters` here
        endpoint.headers?.forEach { request.addValue($0.value, forHTTPHeaderField: $0.key) }
        endpoint.parameters?.forEach {
            request.url?.append(queryItems: [URLQueryItem(name: $0.key, value: $0.value)])
        }
        
        return URLSession.shared.dataTaskPublisher(for: request)
            .tryMap { data, response -> Data in
                
                guard let httpResponse = response as? HTTPURLResponse,
                      (200...299).contains(httpResponse.statusCode) else {
                    throw APIError.invalidResponse
                }
                
                return data
            }
            .decode(type: T.self, decoder: JSONDecoder())
            .subscribe(on: DispatchQueue.global(qos: .background))
            .eraseToAnyPublisher()
    }
}

3. API Model

//
//  Response.swift
//  CombineAPIDemo1
//
//  Created by Joynal Abedin on 31/10/23.
//

import Foundation

struct Response: Codable {
    var responseCode: Int
    var result: String
    var errormessage: String?
    var data: [BookList]
}

struct BookList: Codable {
    var id: Int
    var bookId: Int
    var bookName: String
    var availableCopyNumber: Int
}

4. Get Data

//
//  ContentVM.swift
//  CombineAPIDemo1
//
//  Created by Joynal Abedin on 31/10/23.
//

import Foundation
import Combine

class ContentVM: ObservableObject {
    
    enum ViewState {
        case START
        case LOADING
        case SUCCESS(books: Response)
        case FAILURE(error: String)
    }
    
    @Published var currentState: ViewState = .START
    private var cancelables = Set<AnyCancellable>()
    
    let apiClient = APIService<URLEndpoint>()
    
    init() {
        getBookList()
    }
    
    func getBookList(){
        apiClient.request(URLEndpoint.searchBook(name: "code"))
            .receive(on: DispatchQueue.main)
            .sink { completion in
                switch completion {
                case .finished:
                    print("Execution Finihsed.")
                case .failure(let error):
                    self.currentState = .FAILURE(error: error.localizedDescription)
                }
            } receiveValue: { books in
                self.currentState = .SUCCESS(books: books)
            }.store(in: &cancelables)
    }
    
}