Hello friends, in this article we will talk about how to create a generic service with Swift. With a generic service structure, we can easily change and manage it when we change something extra in the future. We can make service requests with less code.
In the application where we list music, movies, applications and books from Apple’s services, we wrote a different function for each request. It may seem logical to use it in applications with low service requests, but there is no guarantee that it will not grow in the future. So let’s make the service structure of this project generic. The current state is as follows.
// // Services.swift // library // // Created by Ömer Sezer on 13.02.2022. // import Alamofire import AlamofireMapper class Services { let baseUrl = "https://itunes.apple.com/" let limit: Int = 25 func searchMovie(searchedText: String, type: SearchTypes, successCompletion: @escaping ((_ json: MoviesModel?) -> Void), errorCompletion: @escaping ((_ error: BaseErrorModel) -> Void)) { print(type.type) let url = "\(baseUrl)search?term=\(searchedText.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")&offset=\(limit)&limit=\(limit)&media=\(type.type)" Alamofire.request(url, method: .get, encoding: JSONEncoding.default).responseObject { (response: DataResponse<MoviesModel>) in switch response.result { case .success(let model): successCompletion(model) case .failure(let error): errorCompletion(BaseErrorModel(errorCode: nil, message: nil, errors: [ErrorData(field: APIErrors.Alamofire.rawValue, message: error.localizedDescription)])) } } } func searchMovie(searchedText: String, page: Int, type: SearchTypes, successCompletion: @escaping ((_ json: MoviesModel?) -> Void), errorCompletion: @escaping ((_ error: BaseErrorModel) -> Void)) { print(type.type) let url = "\(baseUrl)search?term=\(searchedText.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")&offset=\(limit * page)&limit=\(limit)&media=\(type.type)" Alamofire.request(url, method: .get, encoding: JSONEncoding.default).responseObject { (response: DataResponse<MoviesModel>) in switch response.result { case .success(let model): successCompletion(model) case .failure(let error): errorCompletion(BaseErrorModel(errorCode: nil, message: nil, errors: [ErrorData(field: APIErrors.Alamofire.rawValue, message: error.localizedDescription)])) } } } }
Now we will divide our service structure into 3 different classes. These; APIConstants class where constant variables are kept, APIRouter which has enum structure which requests to be thrown, and Services class where service request is thrown generically.
APIConstants
In this class, there are urls in general and what will go when sending the request.
// // APIConstants.swift // library // // Created by Ömer Sezer on 13.02.2022. // import Foundation struct K { static let prodUrl = "https://itunes.apple.com/" static let limit = 25 } enum HTTPHeaderField: String { case authentication = "Authorization" case contentType = "Content-Type" case acceptType = "Accept" case acceptEncoding = "Accept-Encoding" } enum ContentType: String { case json = "application/json" }
APIRouter
Our structure here is completely enum. When we create a new request, we can add it here as a case, and then we need to determine which method to send, which token value to get, which url to go to, which parameters to take.
As an example, I have added a structure that takes parameters and has different methods. You can go over this.
// // APIRouter.swift // library // // Created by Ömer Sezer on 13.02.2022. // import Alamofire enum APIRouter: URLRequestConvertible { case search(searchedText: String, page: Int, type: SearchTypes) case getExample(exParameter: String) case postExample case getWithToken private var baseURL: String { let serverUrl = K.prodUrl switch self { default: return serverUrl } } private var tokenValue: String { switch self { case .getWithToken: return "Bearer token" default: return "" } } private var method: HTTPMethod { switch self { case .search, .getWithToken, .getExample: return .get case .postExample: return .post } } private var path: String { switch self { case .search(let searchedText, let page, let type): return "search?term=\(searchedText.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")&offset=\(K.limit * page)&limit=\(K.limit)&media=\(type.type)" case .getExample(let exParameter): return "test/\(exParameter)" case .getWithToken: return "test" case .postExample: return "post/test" } } private var parameters: Parameters? { switch self { case .postExample: return ["test": "test"] default: return nil } } func asURLRequest() throws -> URLRequest { let url = try baseURL.appending(path).asURL() var urlRequest = URLRequest(url: url) urlRequest.httpMethod = method.rawValue print(url) print(parameters) print(method) print(tokenValue) switch method { case .get: if let parameters = parameters { do { urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters) } catch { throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) } } default: if let parameters = parameters { do { urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [.prettyPrinted]) } catch { throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) } } } urlRequest.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.acceptType.rawValue) urlRequest.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue) if !tokenValue.isEmpty { urlRequest.setValue(tokenValue, forHTTPHeaderField: HTTPHeaderField.authentication.rawValue) } return urlRequest } }
Services
In this class, we will create a generic function and throw every request through this generic function. I will create a variable named shared with a singleton and proceed over this variable.
// // Services.swift // library // // Created by Ömer Sezer on 13.02.2022. // import Alamofire class Services { static let shared = Services() private func request<T: Decodable>(route: APIRouter, decoder: JSONDecoder = JSONDecoder(), onSuccess: @escaping ((_ data: T?) -> Void), onError: @escaping ((_ error: String) -> Void)) { AF.request(route) .validate(statusCode: 200..<300) .responseDecodable(decoder: decoder) { (response: AFDataResponse<T>) in print(response.result) switch response.result { case .success(let model): onSuccess(model) case .failure(let error): onError(error.localizedDescription) } } } func search(searchedText: String, page: Int = 1, type: SearchTypes, successCompletion: @escaping ((_ data: MoviesModel?) -> Void), errorCompletion: @escaping ((_ error: String) -> Void)) { request(route: .search(searchedText: searchedText, page: page, type: type), onSuccess: successCompletion, onError: errorCompletion) } }
When sending a request, I send it as follows.
// // SearchInteractor.swift // library // // Created by Ömer Sezer on 13.02.2022. // // import UIKit protocol SearchInteractorOutputs: AnyObject { func onMoviesSearched(movies: MoviesModel?) func onNextPageFetched(movies: MoviesModel?) func onError(error: String) } final class SearchInteractor: BaseInteractor, Interactorable { weak var presenter: SearchInteractorOutputs? weak var entities: SearchEntities? func searchMovie(searchedText: String, type: SearchTypes) { Services.shared.search(searchedText: searchedText, type: type) { data in self.presenter?.onMoviesSearched(movies: data) } errorCompletion: { error in self.presenter?.onError(error: error) } } func searchMovie(searchedText: String, pageNumber: Int, type: SearchTypes) { Services.shared.search(searchedText: searchedText, page: pageNumber, type: type) { data in self.presenter?.onNextPageFetched(movies: data) } errorCompletion: { error in self.presenter?.onError(error: error) } } }
If you have questions, you can reach us by sending an e-mail or comment. You can access the project here. You can find more articles here.
Good work.
Leave a Reply