Press ESC to close

Swift Generic Service

Merhaba arkadaşlar, bu yazımızda Swift ile nasıl generic bir servis oluşturulur bundan bahsedeceğiz. Generic bir servis yapısıyla beraber ileride ekstra bir şey değiştirdiğimizde kolayca değiştirebilir ve yönetebiliriz. Daha az kod ile servis isteklerinde bulunabiliriz.

Apple’ın servislerinden müzik, film, uygulama ve kitapları listelediğimiz uygulamada her istek için farklı bir fonksiyon yazmıştık. Az servis isteği atılan uygulamalarda böyle kullanmak mantıklı gibi görünebilir ama ileride büyümeyeceğinin garantisi yok. Bu yüzden bu projenin servis yapısını generic hale getirelim. Şu anki hali aşağıdaki gibidir.

//
//  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)]))
            }
        }
    }
}

Artık servis yapımızı 3 farklı sınıfa ayıracağız. Bunlar; sabit değişkenlerin tutulduğu APIConstants sınıfı, hangi isteklerin atılacağının tutulduğu enum yapıda olan APIRouter ve servis isteğinin generic sekilde atıldığı Services sınıfı.

APIConstants

Bu sınıfta genel olarak urller ve isteği gönderirken neler gidecekse bunlar bulunuyor.

//
//  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

Buradaki yapımız tamamen enum şeklinde. Yeni bir istek oluşturacağımız zaman buraya case şeklinde ekleyebilir ve ardından hangi method ile gönderileceğiniz, hangi token değerini alacağınız, hangi url’e gideceğini, hangi parametreleri alacağını belirlememiz gerekiyor.

Burada örnek olması için parametre alan, farklı metodları bulunan bir yapı ekledim. Bunun üzerinden gidebilirsiniz.

//
//  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

Bu sınıfımızda ise generic bir fonksiyon oluşturup, her isteği bu generic fonksiyon üzerinden atacağız. Singleton ile shared adından bir değişken oluşturup, bu değişken üzerinden ilerleyeceğim.

//
//  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)
    }
}

İstek gönderirken ise aşağıdaki gibi iletiyorum.

//
//  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)
        }
    }
}

Sorularınız olursa mail veya yorum atarak ulaşabilirsiniz. Projeye ise buradan ulaşabilirsiniz. Daha fazla yazıya ise buradan erişebilirsiniz.

İyi çalışmalar.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir