I'm currently trying to find out what's the best networking architecture for MVVM applications. I couldn't find many resources and decided to go with dependency injection based architecture as per the very less reading resources that I have found.
I'm not using any 3rd party for web service testing and whatever the networking architecture that I use should be supported to mock the web services as well.
I have found out the DI based networking architecture which was build intended to achieve unit testing according to the Test Pyramid concept at Apple WWDC 2018.
So I have build my networking layer according to that. Following is my APIHandler class.
public enum HTTPMethod: String { case get = "GET" case post = "POST" case put = "PUT" case patch = "PATCH" case delete = "DELETE" } protocol RequestHandler { associatedtype RequestDataType func makeRequest(from data:RequestDataType) -> URLRequest? } protocol ResponseHandler { associatedtype ResponseDataType func parseResponse(data: Data, response: HTTPURLResponse) throws -> ResponseDataType } typealias APIHandler = RequestHandler & ResponseHandler
Followings are my extensions for request handler and response handler.
extension RequestHandler { func setQueryParams(parameters:[String: Any], url: URL) -> URL { var components = URLComponents(url: url, resolvingAgainstBaseURL: false) components?.queryItems = parameters.map { element in URLQueryItem(name: element.key, value: String(describing: element.value) ) } return components?.url ?? url } func setDefaultHeaders(request: inout URLRequest) { request.setValue(APIHeaders.contentTypeValue, forHTTPHeaderField: APIHeaders.kContentType) } } struct ServiceError: Error,Decodable { let httpStatus: Int let message: String } extension ResponseHandler { func defaultParseResponse<T: Decodable>(data: Data, response: HTTPURLResponse) throws -> T { let jsonDecoder = JSONDecoder() if response.statusCode == 200 { do { let body = try jsonDecoder.decode(T.self, from: data) return body } catch { throw ServiceError(httpStatus: response.statusCode, message: error.localizedDescription) } } else { var message = "Generel.Message.Error".localized() do { let body = try jsonDecoder.decode(APIError.self, from: data) if let err = body.fault?.faultstring { message = err } } catch { throw ServiceError(httpStatus: response.statusCode, message: error.localizedDescription) } throw ServiceError(httpStatus: response.statusCode, message:message) } } }
Then I loaded my request using APILoader as follows.
struct APILoader<T: APIHandler> { var apiHandler: T var urlSession: URLSession init(apiHandler: T, urlSession: URLSession = .shared) { self.apiHandler = apiHandler self.urlSession = urlSession } func loadAPIRequest(requestData: T.RequestDataType, completionHandler: @escaping (Int, T.ResponseDataType?, ServiceError?) -> ()) { if let urlRequest = apiHandler.makeRequest(from: requestData) { urlSession.dataTask(with: urlRequest) { (data, response, error) in if let httpResponse = response as? HTTPURLResponse { guard error == nil else { completionHandler(httpResponse.statusCode, nil, ServiceError(httpStatus: httpResponse.statusCode, message: error?.localizedDescription ?? "General.Error.Unknown".localized())) return } guard let responseData = data else { completionHandler(httpResponse.statusCode,nil, ServiceError(httpStatus: httpResponse.statusCode, message: error?.localizedDescription ?? "General.Error.Unknown".localized())) return } do { let parsedResponse = try self.apiHandler.parseResponse(data: responseData, response: httpResponse) completionHandler(httpResponse.statusCode, parsedResponse, nil) } catch { completionHandler(httpResponse.statusCode, nil, ServiceError(httpStatus: httpResponse.statusCode, message: CommonUtil.shared.decodeError(err: error))) } } else { guard error == nil else { completionHandler(-1, nil, ServiceError(httpStatus: -1, message: error?.localizedDescription ?? "General.Error.Unknown".localized())) return } completionHandler(-1, nil, ServiceError(httpStatus: -1, message: "General.Error.Unknown".localized())) } }.resume() } } }
To call my API request. I have created a separate service class and call the web service as follows.
struct TopStoriesAPI: APIHandler { func makeRequest(from param: [String: Any]) -> URLRequest? { let urlString = APIPath().topStories if var url = URL(string: urlString) { if param.count > 0 { url = setQueryParams(parameters: param, url: url) } var urlRequest = URLRequest(url: url) setDefaultHeaders(request: &urlRequest) urlRequest.httpMethod = HTTPMethod.get.rawValue return urlRequest } return nil } func parseResponse(data: Data, response: HTTPURLResponse) throws -> StoriesResponse { return try defaultParseResponse(data: data,response: response) } }
For syncing both my actual web service methods and mock services, I have created an API Client protocol like follows.
protocol APIClientProtocol { func fetchTopStories(completion: @escaping (StoriesResponse?, ServiceError?) -> ()) }
Then I have derived APIServices class using my APIClient protocol and implemented my all the APIs there by passing requests and responses. My dependency injection was getting over at this point.
public class APIServices: APIClientProtocol { func fetchTopStories(completion: @escaping (StoriesResponse?, ServiceError?) -> ()) { let request = TopStoriesAPI() let params = [Params.kApiKey.rawValue : CommonUtil.shared.NytApiKey()] let apiLoader = APILoader(apiHandler: request) apiLoader.loadAPIRequest(requestData: params) { (status, model, error) in if let _ = error { completion(nil, error) } else { completion(model, nil) } } } }
Then I have called this API request on my viewModel class like this.
func fetchTopStories(completion: @escaping (Bool) -> ()) { APIServices().fetchTopStories { response, error in if let _ = error { self.errorMsg = error?.message ?? "Generel.Message.Error".localized() completion(false) } else { if let data = response?.results { if data.count > 0 { self.stories.removeAll() self.stories = data completion(true) } else { self.errorMsg = "Generel.NoData.Error".localized() completion(false) } } else { self.errorMsg = "Generel.NoData.Error".localized() completion(false) } } } }
Finally call the viewModel's API call from my viewController (View).
func fetchData() { showActivityIndicator() self.viewModel.fetchTopStories { success in self.hideActivityIndicator() DispatchQueue.main.async { if self.pullToRefresh { self.pullToRefresh = false self.refreshControl.endRefreshing() } if success { if self.imgNoData != nil { self.imgNoData?.isHidden = true } self.tableView.reloadData() } else { CommonUtil.shared.showToast(message: self.viewModel.errorMsg, success: false) self.imgNoData = { let viewWidth = self.tableView.frame.size.width let imageWidth = viewWidth - 50 let iv = UIImageView() iv.frame = CGRect(x: 25, y: 100, width: imageWidth, height: imageWidth) iv.image = UIImage(named:"no-data") iv.contentMode = .scaleAspectFit return iv }() self.imgNoData?.isHidden = false self.tableView.addSubview(self.imgNoData!) } } } }
So I have following questions regarding this approach.
- I have ended the dependency injection from my APIServices class. Should I bring this all the way up to my viewController class API Call and pass
request
andparams
variables from there ? - Are there any performance issues in this approach and any improvement to be done?
- My personal preference is to end all the data related stuffs from the viewModel level and just call the API without passing any parameters from the viewController. Does it wrong? If we pass parameters from the view controller class as per the pure dependency injection way, does it harm to the MVVM architecture?