Press ESC to close

Swift Generic Service

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

Your email address will not be published. Required fields are marked *