Call function with completion handler - swift

I created the function below but I'm not sure how to call it, it complains saying:
Type 'T.Type' cannot conform to 'Decodable'
Here's how I'd like to call it:
let result = getApiData(modelToDecode: MyModel, url: "abc")
This is what I've tried:
func getApiData<T : Decodable>(modelToDecode: T.Type, url: String) -> Any? {
// I get an error below
fetchDataAndDecode(url: String, modelToDecode: T.Type) { result in
}
// temp placeholder
return nil
}
func fetchDataAndDecode<T : Decodable>(url: String, modelToDecode: T.Type, completionHandler: #escaping (Result<T.Type, NetworkError>) -> Void) {
guard let url = URL(string: url) else {
completionHandler(.failure(NetworkError.badURL))
return
}
AF.request(url, method: .get).validate().responseData { response in
guard let data = response.data else {
completionHandler(.failure(NetworkError.apiFailed))
return
}
do {
// Decode the data
let decodedData = try JSONDecoder().decode(modelToDecode.self, from: data)
DispatchQueue.main.async {
completionHandler(.success(decodedData as! T.Type))
}
} catch(let error) {
print("🛑 Error on afRequest(): \(error)")
}
}
}
How can I call it inside the class properly?

Result should be Result<T, NetworkError>. No need to add modelToDecode to your method declaration. You can explicitly set the resulting type in your async call. Btw you should the completion handler as well if you fail to decode your data:
enum NetworkError: Error {
case badURL, apiFailed, corruptedData
}
Your method should look like this:
func fetchDataAndDecode<T: Decodable>(url: String, completionHandler: #escaping (Result<T, NetworkError>) -> Void) {
guard let url = URL(string: url) else {
completionHandler(.failure(.badURL))
return
}
AF.request(url, method: .get).validate().responseData { response in
guard let data = response.data else {
completionHandler(.failure(.apiFailed))
return
}
do {
let decodedData = try JSONDecoder().decode(T.self, from: data)
DispatchQueue.main.async {
completionHandler(.success(decodedData))
}
} catch {
completionHandler(.failure(.corruptedData))
}
}
}
And when calling it you need to explicitly set the resulting type:
fetchDataAndDecode(url: "yourURL") { (result: Result<WhatEver, NetworkError>) in
// switch the result here
}

Related

Swift generics in completion handler

Im trying to refactor dat fetching func to enable it for several Decodable struct types.
func fetchData<T: Decodable>(_ fetchRequest: FetchRequestType, completion: #escaping ((Result<T, Error>) -> Void)) {
guard !isFetching else { return }
isFetching = true
guard let url = getURL(fetchRequest) else { assertionFailure("Could not compose URL"); return }
let urlRequest = URLRequest(url: url)
self.session.dataTask(with: urlRequest) { [unowned self] (data, response, error) in
guard let response = response as? HTTPURLResponse,
response.statusCode == 200 else {
self.isFetching = false
completion(.failure(NSError()))
return
}
guard let data = data else { assertionFailure("No data"); return }
if let jsonData = try? JSONDecoder().decode(T.self, from: data) {
self.isFetching = false
completion(.success(jsonData))
} else {
assertionFailure("Could not decode JSON data"); return
}
}.resume()
}
But when Im calling the func from controller with one of Decodable types I get a compile error
Generic parameter 'T' could not be inferred
networkClient.fetchData(.accountsSearch(searchLogin: text, pageNumber: 1)) { [unowned self] result in
switch result {
case .success(let dataJSON):
let accountsListJSON = dataJSON as! AccountsListJSON
let fetchedAccounts = accountsListJSON.items
.map({ AccountGeneral(login: $0.login, id: $0.id, avatarURLString: $0.avatarURL, type: $0.type) })
self.accounts = fetchedAccounts
case .failure(_):
assertionFailure("Fetching error!")
}
}
Please help me to find out what happened and solve a problem.
You can generally help the compiler to infer the T type by providing the result type, when you call fetchData(_:completion:) function like this:
networkClient.fetchData(
.accountsSearch(searchLogin: text, pageNumber: 1)
) { [unowned self] (result: Result<AccountsListJSON, Error>) in
...
}
If the method doesn't have a return type where the static type can be specified you have to add a parameter
func fetchData<T: Decodable>(_ fetchRequest: FetchRequestType, type: T.Type, completion: #escaping (Result<T, Error>) -> Void) { ...
and call it
networkClient.fetchData(.accountsSearch(searchLogin: text, pageNumber: 1), type: AccountsListJSON.self) { [unowned self] result in
and delete the downcast as! AccountsListJSON

Call completion Handler function using "Result"

How to use call getEmployee function while using "Result"?
struct Employee {
let name: String
let designation: String
}
func getEmployee(name: String, completion: #escaping(Result<Employee?, Error>) -> Void) {
}
First you need to make your structure conform to Codable
struct Employee: Codable {
let name, designation: String
}
Than you need to fetch your data from your server and call completion if decoding was successful with your employee .success(employee) or if it fails pass failure with your error .failure(error):
func getEmployee(name: String, completion: #escaping(Result<Employee, Error>) -> Void) {
URLSession.shared.dataTask(with: URL(string: "http://www.example.com/getEmployee.api?name=\(name)")!) { data, response, error in
guard let data = data, error == nil else {
if let error = error { completion(.failure(error)) }
return
}
do {
let employee = try JSONDecoder().decode(Employee.self, from: data)
completion(.success(employee))
} catch {
completion(.failure(error))
}
}
}
Usage:
getEmployee(name: "Ella") { result in
switch result {
case let .success(employee):
print("employee:", employee)
case let .failure(error):
print("error:", error)
default: break
}
}

Write unit test for function that uses URLSession and RxSwift

I have a function that creates and returns Observable that downloads and decodes data using URLSession. I wanted to write unit test for this function but have no idea how to tackle it.
function:
func getRecipes(query: String, _ needsMoreData: Bool) -> Observable<[Recipes]> {
guard let url = URL(string: "https://api.spoonacular.com/recipes/search?\(query)&apiKey=myApiKey") else {
return Observable.just([])
}
return Observable.create { observer in
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {
return
}
do {
if self.recipes == nil {
self.recipes = try self.decoder.decode(Recipes.self, from: data)
self.dataList = self.recipes.results
self.baseUrl = self.recipes.baseUrl
} else {
if needsMoreData {
self.recipes = try self.decoder.decode(Recipes.self, from: data)
self.dataList.append(contentsOf: self.recipes.results.suffix(50))
} else {
self.dataList = try self.decoder.decode(Recipes.self, from: data).results
}
}
observer.onCompleted()
} catch let error {
observer.onError(error)
}
}
task.resume()
return Disposables.create {
task.cancel()
}
}
.trackActivity(activityIndicator)
}
The obvious answer is to inject the dataTask instead of using the singleton inside your function. Something like this:
func getRecipes(query: String, _ needsMoreData: Bool, dataTask: #escaping (URL, #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask) -> Observable<[Recipes]> {
guard let url = URL(string: "https://api.spoonacular.com/recipes/search?\(query)&apiKey=myApiKey") else {
return Observable.just([])
}
return Observable.create { observer in
let task = dataTask(url) { (data, response, error) in
// and so on...
You would call it in the main code like this:
getRecipes(query: "", false, dataTask: URLSession.shared.dataTask(with:completionHandler:))
In your test, you would need something like this:
func fakeDataTask(_ url: URL, _ completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
XCTAssertEqual(url, expectedURL)
completionHandler(testData, nil, nil)
return URLSessionDataTask()
}
let result = getRecipes(query: "", false, dataTask: fakeDataTask)
Did you know that URLSession has Reactive extensions already created for it? The one I like best is: URLSession.shared.rx.data(request:) which returns an Observable which will emit an error if there are any problems getting the data. I suggest you use it.

Generic Decoder for Swift using a protocol

I tried to use a generic Json Decoder for all of my models using a protrocol.
//Here the definition of the protocol:
func fetch<T: Decodable>(with request: URLRequest, decode: #escaping (Decodable) -> T?, completion: #escaping (Result<T, APIError>) -> Void) {.. other Code}
//Here the implementation:
func getData(from endPoint: Endpoint, completion: #escaping (Result<ApiResponseArray<Codable>, APIError>) -> Void) {
let request = endPoint.request
fetch(with: request, decode: { json -> Decodable in
guard let dataResult = json as? modelData else { return nil }
return dataResult
}, completion: completion)
}
ApiResponseArray gives me the error: Protocol type 'Codable' (aka 'Decodable & Encodable') cannot conform to 'Decodable' because only concrete types can conform to protocols. But how can I implement a generic decoder and passing them different models. I think I have to modify my protocol definition but how? I would like to pass the model and then receive the decoded data for the model (in my example modelData). Its obvious that the program runs when I write:
func getData(from endPoint: Endpoint, completion: #escaping (Result, APIError>) I mean when I use the concrete Model, but I want to pass the model, so that I can use the class for different models.
Thanks,
Arnold
A protocol cannot conform to itself, Codable must be a concrete type or can only be used as a generic constraint.
In your context you have to do the latter, something like this
func fetch<T: Decodable>(with request: URLRequest, decode: #escaping (Data) throws -> T, completion: #escaping (Result<T, APIError>) -> Void) { }
func getData<T: Decodable>(_ : T.Type = T.self, from endPoint: Endpoint, completion: #escaping (Result<T, APIError>) -> Void) {
let request = endPoint.request
fetch(with: request, decode: { data -> T in
return try JSONDecoder().decode(T.self, from: data)
}, completion: completion)
}
A network request usually returns Data which is more reasonable as parameter type of the decode closure
I can suggest to you how to use Decodable with your API call structure by using Alamofire.
I have created RequestManager class which inherits from SessionManager and added request call inside which common to all.
class RequestManager: SessionManager {
// Create shared instance
static let shared = RequestManager()
// Create http headers
lazy var httpHeaders : HTTPHeaders = {
var httpHeader = HTTPHeaders()
httpHeader["Content-Type"] = "application/json"
httpHeader["Accept"] = "application/json"
return httpHeader
}()
//------------------------------------------------------------------------------
// MARK:-
// MARK:- Request Methods
//------------------------------------------------------------------------------
func responseRequest(_ url: String, method: Alamofire.HTTPMethod, parameter: Parameters? = nil, encoding: ParameterEncoding, header: HTTPHeaders? = nil, completionHandler: #escaping (DefaultDataResponse) -> Void) -> Void {
self.request(url, method: method, parameters: parameter, encoding: encoding, headers: header).response { response in
completionHandler(response)
}
}
}
Then after one more class created NetworkManager class which hold required get/post method call and decode json by JSONDecoder as follow:
class NetworkManager {
static let shared = NetworkManager()
var progressVC : ProgressVC?
//----------------------------------------------------------------
// MARK:-
// MARK:- Get Request Method
//----------------------------------------------------------------
func getResponse<T: Decodable>(_ url: String, parameter: Parameters? = nil, encoding: ParameterEncoding = URLEncoding.default, header: HTTPHeaders? = nil, showHUD: HUDFlag = .show, message: String? = "Please wait...", decodingType: T.Type, completion: #escaping (Decodable?, APIError?) -> Void) {
DispatchQueue.main.async {
self.showHideHud(showHUD: showHUD, message: "")
}
RequestManager.shared.responseRequest(url, method: .get, parameter: parameter, encoding: encoding, header: header) { response in
DispatchQueue.main.async {
self.showHideHud(showHUD: .hide, message: "")
}
guard let httpResponse = response.response else {
completion(nil, .requestFailed("Request Failed"))
return
}
if httpResponse.statusCode == 200 {
if let data = response.data {
do {
let genericModel = try JSONDecoder().decode(decodingType, from: data)
completion(genericModel, nil)
} catch {
do {
let error = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]
if let message = error!["message"] as? String {
completion(nil, .errorMessage(message)!)
} else if let message = error!["message"] as? Int {
completion(nil, .errorMessage(String(describing: "Bad Request = \(message)")))
}
} catch {
completion(nil, .jsonConversionFailure("JSON Conversion Failure"))
}
}
} else {
completion(nil, .invalidData("Invalid Data"))
}
} else {
completion(nil, .responseUnsuccessful("Response Unsuccessful"))
}
}
}
}
ProgressVC is my custom class to show progress view when api call.
After that, I have created DataManager class which will help me to create request url.
class DataManager: NSObject {
//------------------------------------------------------------------------------
// MARK:- Variables
//------------------------------------------------------------------------------
static let shared = DataManager()
let baseUrl = WebServiceURL.local
//------------------------------------------------------------------------------
// MARK:- Custom Methods
//------------------------------------------------------------------------------
// Get API url with endpoints
func getURL(_ endpoint: WSEndPoints) -> String {
return baseUrl + endpoint.rawValue
}
}
I have created following enum to send data or error in my completion block.
enum Result<T, U> where U: Error {
case success(T)
case failure(U)
}
Here is list of error which stored custom message related to status fired during api call.
enum APIError: Error {
case errorMessage(String)
case requestFailed(String)
case jsonConversionFailure(String)
case invalidData(String)
case responseUnsuccessful(String)
case jsonParsingFailure(String)
var localizedDescription: String {
switch self {
case.errorMessage(let msg):
return msg
case .requestFailed(let msg):
return msg
case .jsonConversionFailure(let msg):
return msg
case .invalidData(let msg):
return msg
case .responseUnsuccessful(let msg):
return msg
case .jsonParsingFailure(let msg):
return msg
}
}
}
Then after, I will extend this DataManager class to call web service based on module. So I will create Swift file and extend DataManager class and call relative API.
See following, In API call I will return relative model into Result like Result<StoreListModel, APIError>
extension DataManager {
// MARK:- Store List
func getStoreList(completion: #escaping (Result<StoreListModel, APIError>) -> Void) {
NetworkManager.shared.getResponse(getURL(.storeList), parameter: nil, encoding: JSONEncoding.default, header: getHeaders("bd_suvlascentralpos"), showHUD: .show, message: "Please wait...", decodingType: StoreListModel.self) { (decodableData, apiError) in
if apiError != nil {
completion(.failure(apiError!))
} else {
guard let userData = decodableData as? StoreListModel else {
completion(.failure(apiError!))
return
}
completion(.success(userData))
}
}
}
}
From completion block of request I will get decodable data which here safely type cast.
Use:
DataManager.shared.getStoreList { (result) in
switch result {
case .success(let storeListModel):
if let storeList = storeListModel, storeList.count > 0 {
self.arrStoreList = storeList
self.tblStoreList.isHidden = false
self.labelEmptyData.isHidden = true
self.tblStoreList.reloadData()
} else {
self.tblStoreList.isHidden = true
self.labelEmptyData.isHidden = false
}
break
case .failure(let error):
print(error.localizedDescription)
break
}
}
Note:- Some variables, models classes are my custom. You can replace it with your.

do not know how to get the result of completion

I am having trouble to use the result of a completion handler.
I am getting this error "Cannot convert value of type '()' to expected argument type"
struct SearchCollectionViewModel {
let name: String
let previewURL: String?
var image:UIImage?
let dataController = DataController()
}
extension SearchCollectionViewModel {
init(with result: Result) {
self.name = result.trackName
self.previewURL = result.previewURL
if let url = result.previewURL {
let imgData = preview(with: url, completion: { data -> Data? in
guard let data = data as? Data else { return nil }
return data
})
self.image = UIImage(data: imgData)
}
}
private func preview(with url: String, completion: #escaping (Data) -> Data?) {
dataController.download(with: url) { data, error in
if error == nil {
guard let imageData = data else { return }
DispatchQueue.main.async {
_ = completion(imageData)
}
}
}
}
}
A couple of observations:
You cannot “return” a value that is retrieved asynchronously via escaping closure.
The closure definition (Data) -> Data? says that the closure not only will be passed the Data retrieved for the image, but that the closure will, itself, return something back to preview. But it’s obviously not doing that (hence the need for _, as in _ = completion(...)). I’d suggest you change that to (Data?) -> Void (or use the Result<T, U> pattern).
I’d suggest renaming your Result type as there’s a well-known generic called Result<Success, Failure> for returning .success(Success) or .failure(Failure). This is a pattern that we’ve used for a while, but is formally introduced in Swift 5, too. See SE-0235.
Your codebase can have its own Result type, but it’s an invitation for confusion later down the road if and when you start adopting this Result<T, U> convention.
You really shouldn’t be initiating asynchronous process from init, but instead invoke a method to do that.
Personally, I’d move the conversion to UIImage into the DataController, e.g.
extension DataController {
func downloadImage(with url: URL, completion: #escaping (UIImage?, Error?) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, _, error in
let image = data.flatMap { UIImage(data: $0) }
completion(image, error)
}
task.resume()
}
}
So, I might suggest you end up with something like:
class SearchCollectionViewModel {
let name: String
let previewURL: String?
let dataController = DataController()
var image: UIImage?
init(with result: Result) {
self.name = result.trackName
self.previewURL = result.previewURL
}
}
extension SearchCollectionViewModel {
func preview(with url: String, completion: #escaping (UIImage?) -> Void) {
guard let urlString = previewURL, let url = URL(string: urlString) else {
completion(nil)
return
}
dataController.downloadImage(with: url) { [weak self] image, error in
DispatchQueue.main.async {
self?.image = image
completion(image)
}
}
}
}