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