I have a function which I use to make api call but sometimes, I could get a decaying error and debugging this has been difficult. Below is my function, is there any way to maybe log or print the bad decoding key since I am using combine.
func request<T>(with builder: BaseRequest, customDecoder: JSONDecoder) -> AnyPublisher<T, APIError> where T: Codable {
let encoding: ParametersEncoder = [.get, .delete].contains(builder.method) ? URLParameretersEncoder() : JSONParametersEncoder()
customDecoder.keyDecodingStrategy = .convertFromSnakeCase
var url: URL {
var components = URLComponents()
components.scheme = "http"
components.host = builder.baseUrl
components.path = "/api/v1" + builder.path
guard let url = components.url else {
preconditionFailure("Invalid URL components: \(components)")
}
return url
}
var urlRequest = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 46.0)
urlRequest.httpMethod = builder.method.rawValue
builder.headers.forEach { key, value in
urlRequest.setValue(value, forHTTPHeaderField: key)
}
if let token = tokenManager.token {
urlRequest.setValue("Bearer " + token, forHTTPHeaderField: "Authorization")
}
if let parameters = builder.parameters {
guard let encoded = try? encoding.encode(parameters: parameters, in: urlRequest) else {
fatalError()
}
urlRequest = encoded
}
self.log(request: urlRequest)
return URLSession.shared
.dataTaskPublisher(for: urlRequest)
.receive(on: DispatchQueue.main)
.mapError { _ in .unknown }
.flatMap { data, response -> AnyPublisher<T, APIError> in
guard let response = response as? HTTPURLResponse else {
return Fail(error: APIError.invalidResponse).eraseToAnyPublisher()
}
self.log(response: response, data: data, error: nil)
if (200...299).contains(response.statusCode) {
return Just(data)
.decode(type: T.self, decoder: customDecoder)
// .map {
// print($0)
// return $0
// } //added
.mapError {_ in .decodingError}
.eraseToAnyPublisher()
} else {
guard let errorResponse = try? customDecoder.decode(BaseResponse.self, from: data) else {
return Fail(error: APIError.decodingError).eraseToAnyPublisher()
}
return Fail(error: APIError.server(response: errorResponse))
.eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
}
I have been using this snippet to point to the bad decoding key which might help.
if let data = response.data {
do {
let decoder = JSONDecoder()
let messages = try decoder.decode(T.self, from: data)
print(messages as Any)
} catch DecodingError.dataCorrupted(let context) {
print(context)
} catch DecodingError.keyNotFound(let key, let context) {
print("Key '\(key)' not found:", context.debugDescription)
print("codingPath:", context.codingPath)
} catch DecodingError.valueNotFound(let value, let context) {
print("Value '\(value)' not found:", context.debugDescription)
print("codingPath:", context.codingPath)
} catch DecodingError.typeMismatch(let type, let context) {
print("Type '\(type)' mismatch:", context.debugDescription)
print("codingPath:", context.codingPath)
} catch {
print("error: ", error)
}
}
Note that only include it for debugging purposes if decoding an API is not working as expected, and remove it after you have made necessary changes after seeing its output.
Related
I am trying to understand what is going on in my code here.
I have a simple API call to open weahter API and that whenever the user taps the UIButton, it should call the api and get the data back from open weather.
Everything works as intended however, when I have my UIButton pressed, the print statement executed first before the Task closure. I'm trying to understand the race condition here
This is my code in viewController:
#IBAction func callAPIButton(_ sender: UIButton) {
Task {
let weatherData = await weatherManager.fetchWeather(cityName: "Seattle")
}
}
Here's the code for fetching the API:
struct WeatherManager{
let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=someAPIKeyHere"
func fetchWeather(cityName: String) -> WeatherModel? {
let urlString = "\(weatherURL)&q=\(cityName)"
let requestResult = performRequest(urlString: urlString)
return requestResult
}
func performRequest(urlString: String) -> WeatherModel? {
var weatherResult : WeatherModel? = nil
if let url = URL(string: urlString){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url, completionHandler: {
(data, response, error) in
if error != nil {
return
}
if let safeData = data {
weatherResult = parseJSON(weatherData: safeData)
}
})
task.resume()
}
return weatherResult
}
func parseJSON(weatherData: Data) -> WeatherModel?{
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(WeatherResponse.self, from: weatherData)
print("this is in decodedData: \(decodedData)")
let temp = decodedData.main.temp
let name = decodedData.name
let weather = WeatherModel(conditionId:300, cityName: name, temperature: temp)
return weather
} catch {
print("Something is wrong here: " + error.localizedDescription)
}
return nil
}
}
Here's my Model:
struct WeatherModel{
let conditionId: Int
let cityName: String
let temperature: Double
var temperatureString: String{
return String(format: "%.1f", temperature)
}
var conditionName: String {
switch conditionId {
case 200...232:
return "cloud.bolt"
case 300...321:
return "cloud.drizzle"
case 500...531:
return "cloud.rain"
case 600...622:
return "cloud.snow"
case 701...781:
return "cloud.fog"
case 800:
return "sun.max"
case 801...804:
return "cloud.bolt"
default:
return "cloud"
}
}
}
Desired result:
This is in weatherData: WeatherResponse(name: "Seattle", weather: [Awesome_Weather_App.WeatherAPI(description: "overcast clouds", icon: "04d")], main: Awesome_Weather_App.MainAPI(temp: 287.81, pressure: 1018.0, humidity: 44.0, temp_min: 284.91, temp_max: 290.42, feels_like: 286.48), sys: Awesome_Weather_App.SysAPI(sunrise: 1.6712886e+09, sunset: 1.6713243e+09))
This is what I am getting instead:
This is in weatherData: nil
this is in decodedData: WeatherResponse(name: "Seattle", weather: [Awesome_Weather_App.WeatherAPI(description: "overcast clouds", icon: "04d")], main: Awesome_Weather_App.MainAPI(temp: 287.81, pressure: 1018.0, humidity: 44.0, temp_min: 284.91, temp_max: 290.42, feels_like: 286.48), sys: Awesome_Weather_App.SysAPI(sunrise: 1.6712886e+09, sunset: 1.6713243e+09))
Thank you in advance
Everything works as intended
No, it doesn't. I don't know why you claim such a thing; your code isn't working at all.
The problem is that you are trying to return weatherResult from performRequest. But performRequest gets its weatherResult value asynchronously, so this attempt is doomed to failure; you will always be returning nil, because the return weatherResult happens before session.dataTask ever even starts to find out what weatherResult is.
You cannot just synchronously return the results of an asynchronous request. You have two basic options for asynchronous requests.
Use the older “completion handler” pattern with Result types:
struct WeatherManager {
let weatherURL = "https://api.openweathermap.org/data/2.5/weather"
let appId = "someAPIKeyHere"
func fetchWeather(
cityName: String,
completion: #escaping (Result<WeatherModel, Error>) -> Void
) {
guard var components = URLComponents(string: weatherURL) else {
completion(.failure(URLError(.badURL)))
return
}
components.queryItems = [
URLQueryItem(name: "appid", value: appId),
URLQueryItem(name: "q", value: cityName)
]
guard let url = components.url else {
completion(.failure(URLError(.badURL)))
return
}
performRequest(url: url, completion: completion)
}
func performRequest(
url: URL,
queue: DispatchQueue = .main,
completion: #escaping (Result<WeatherModel, Error>) -> Void
) {
let session = URLSession.shared // note, do not create a new URLSession for every request or else you will leak; use shared instance
let task = session.dataTask(with: url) { data, response, error in
guard
error == nil,
let data = data,
let response = response as? HTTPURLResponse,
200 ..< 300 ~= response.statusCode
else {
queue.async { completion(.failure(error ?? URLError(.badServerResponse))) }
return
}
do {
let weatherResult = try parseJSON(weatherData: data)
queue.async { completion(.success(weatherResult)) }
} catch {
queue.async { completion(.failure(error)) }
}
}
task.resume()
}
func parseJSON(weatherData: Data) throws -> WeatherModel {
let decoder = JSONDecoder()
let response = try decoder.decode(WeatherResponse.self, from: weatherData)
print("this is in decodedData: \(response)")
return WeatherModel(conditionId: 300, cityName: response.name, temperature: response.main.temp)
}
}
Then, rather than:
let weather = weatherManager.fetchWeather(cityName: …)
You would
weatherManager.fetchWeather(cityName: …) { result in
switch result {
case .failure(let error):
print(error)
case .success(let weather):
// do something with the `weather` object here
}
}
// note, do not do anything with `weather` here, because the above
// runs asynchronously (i.e., later).
Use the newer async-await pattern of Swift concurrency:
struct WeatherManager {
let weatherURL = "https://api.openweathermap.org/data/2.5/weather"
let appId = "someAPIKeyHere"
func fetchWeather(cityName: String) async throws -> WeatherModel {
guard var components = URLComponents(string: weatherURL) else {
throw URLError(.badURL)
}
components.queryItems = [
URLQueryItem(name: "appid", value: appId),
URLQueryItem(name: "q", value: cityName)
]
guard let url = components.url else {
throw URLError(.badURL)
}
return try await performRequest(url: url)
}
func performRequest(url: URL) async throws -> WeatherModel {
let session = URLSession.shared // note, do not create a new URLSession for every request or else you will leak; use shared instance
let (data, response) = try await session.data(from: url)
guard
let response = response as? HTTPURLResponse,
200 ..< 300 ~= response.statusCode
else {
throw URLError(.badServerResponse)
}
return try parseJSON(weatherData: data)
}
func parseJSON(weatherData: Data) throws -> WeatherModel {
let decoder = JSONDecoder()
do {
let response = try decoder.decode(WeatherResponse.self, from: weatherData)
print("this is in decodedData: \(response)")
return WeatherModel(conditionId: 300, cityName: response.name, temperature: response.main.temp)
} catch {
print("Something is wrong here: " + error.localizedDescription)
throw error
}
}
}
And then you can do things like:
Task {
do {
let weather = try await weatherManager.fetchWeather(cityName: …)
// do something with `weather` here
} catch {
print(error)
}
}
Note, a few changes in the above unrelated to the asynchronous nature of your request:
Avoid creating URLSession instances. If you do, you need to remember to invalidate them. Instead, it is much easier to use URLSession.shared, eliminating this annoyance.
Avoid building URLs with string interpolation. Use URLComponents to build safe URLs (e.g., ones that can handle city names like “San Francisco”, with spaces in their names).
I am use URLSession for POST request and obtain list of players, but sometimes I am obtain error "Error Domain=NSURLErrorDomain Code=-1001", how I can handle it?
How I am get error inside "completion"?
func addPlayer(playerCode: String, completion: #escaping (Error?) -> ()) {
guard let url = URL(string: "\(url)") else { return }
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
let playerCode = [playerCode]
let params = ["players": playerCode]
do {
let data = try JSONSerialization.data(withJSONObject: params, options: .init())
urlRequest.httpBody = data
urlRequest.setValue("application/json", forHTTPHeaderField: "content-type")
let sessionConfig = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = 5.0
DispatchQueue.global(qos: .background).async {
URLSession(configuration: sessionConfig).dataTask(with: urlRequest) { (data, resp, err) in
guard let data = data else { return }
guard let resp = resp else { return }
print(data)
print(resp)
guard let response = resp as? HTTPURLResponse, (200 ..< 530) ~= response.statusCode else {
print("Error: HTTP request failed")
return
}
if response.statusCode == 200 {
print("Status code is 200")
do {
let json = try JSONDecoder().decode(ListOfPlayers.self, from: data)
// I am do what I am want
} catch let jsonError {
print("Error json serialization \(jsonError)")
}
completion(nil)
} else if response.statusCode == 422 {
print("Status code is 422")
completion(nil)
return
}
}.resume()
}
} catch {
}
}
Also I am try use this code:
guard let err = err else { return }
if (err._code == -1001) {
print("Oops bad connection")
}
but this code don't executed during "Time out"
You can cast your error to URLError type and then use the code property to safely check if it's a session timeout error:
if (err as? URLError)?.code == .timedOut {
// Handle session timeout
}
I'm having an issue decoding an API response.
So we have a NetworkManager class which we use to decode APIs. I have a simple GET endpoint that I need to retrieve a list of airports from. Here is the endpoint:
static let airports = Endpoint(url: "/test/airports")
Endpoint is defined as follows:
public struct Endpoint : Equatable {
public init(url: String? = nil, pattern: String? = nil, methods: [Test.HTTPMethod] = [.get], type: Test.EncodingType = .json)
}
Then in our network manager we have:
public func call<R: Decodable>(_ endpoint: Endpoint,
with args: [String: String]? = nil,
using method: HTTPMethod = .get,
expecting response: R.Type?,
completion: APIResponse<R>) {
call(endpoint, with: args, parameters: Nothing(),
using: method, posting: Nothing(), expecting: response, completion: completion)
}
My Airport model is as follows:
struct Airport: Codable {
let id: String
let name: String
let iata3: String
let icao4: String
let countryCode: String
}
And then I'm calling the endpoint like:
private func getAirportsList() {
API.client.call(.airports, expecting: [Airport].self) { (result, airports) in
print(airports)
}
}
Now I'm using Charles to proxy and I am getting the response I expect:
[{
"id": "5f92b0269c983567fc4b9683",
"name": "Amsterdam Schiphol",
"iata3": "AMS",
"icao4": "EHAM",
"countryCode": "NL"
}, {
"id": "5f92b0269c983567fc4b9685",
"name": "Bahrain International",
"iata3": "BAH",
"icao4": "OBBI",
"countryCode": "BH"
}, {
"id": "5f92b0269c983567fc4b968b",
"name": "Bankstown",
"iata3": "BWU",
"icao4": "YSBK",
"countryCode": "AU"
}]
But in my getAirports() method, airports is nil. I'm really struggling to see why. Clearly the endpoint is being hit correctly but my decoding is failing.
Edit:
Full method:
private func call<P: Encodable, B: Encodable, R: Decodable>(_ endpoint: Endpoint,
with args: [String: String]? = nil,
parameters params: P?,
using method: HTTPMethod = .get,
posting body: B?,
expecting responseType: R.Type?,
completion: APIResponse<R>) {
// Prepare our URL components
guard var urlComponents = URLComponents(string: baseURL.absoluteString) else {
completion?(.failure(nil, NetworkError(reason: .invalidURL)), nil)
return
}
guard let endpointPath = endpoint.url(with: args) else {
completion?(.failure(nil, NetworkError(reason: .invalidURL)), nil)
return
}
urlComponents.path = urlComponents.path.appending(endpointPath)
// Apply our parameters
applyParameters: if let parameters = try? params.asDictionary() {
if parameters.count == 0 {
break applyParameters
}
var queryItems = [URLQueryItem]()
for (key, value) in parameters {
if let value = value as? String {
let queryItem = URLQueryItem(name: key, value: value)
queryItems.append(queryItem)
}
}
urlComponents.queryItems = queryItems
}
// Try to build the URL, bad request if we can't
guard let urlString = urlComponents.url?.absoluteString.removingPercentEncoding,
var url = URL(string: urlString) else {
completion?(.failure(nil, NetworkError(reason: .invalidURL)), nil)
return
}
if let uuid = UIDevice.current.identifierForVendor?.uuidString, endpoint.pattern == "/logging/v1/device/<device_id>" {
let us = "http://192.168.6.128:3000/logging/v1/device/\(uuid)"
guard let u = URL(string: us) else { return }
url = u
}
// Can we call this method on this endpoint? If not, lets not try to continue
guard endpoint.httpMethods.contains(method) else {
completion?(.failure(nil, NetworkError(reason: .methodNotAllowed)), nil)
return
}
// Apply debug cookie
if let debugCookie = debugCookie {
HTTPCookieStorage.shared.setCookies(
HTTPCookie.cookies(
withResponseHeaderFields: ["Set-Cookie": debugCookie],
for:url
), for: url, mainDocumentURL: url)
}
// Build our request
var request = URLRequest(url: url)
request.httpMethod = method.rawValue
if let headers = headers {
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key)
}
}
// If we are posting, safely retrieve the body and try to assign it to our request
if !(body is NothingProtocol) {
guard let body = body else {
completion?(.failure(nil, NetworkError(reason: .buildingPayload)), nil)
return
}
do {
let result = try encode(body: body, type: endpoint.encodingType)
request.httpBody = result.data
request.setValue(result.headerValue, forHTTPHeaderField: "Content-Type")
} catch {
completion?(.failure(nil, NetworkError(reason: .buildingPayload)), nil)
return
}
}
// Build our response handler
let task = session.dataTask(with: request as URLRequest) { (rawData, response, error) in
// Print some logs to help track requests
var debugOutput = "URL\n\(url)\n\n"
if !(params is Nothing.Type) {
debugOutput.append(contentsOf: "PARAMETERS\n\(params.asJSONString() ?? "No Parameters")\n\n")
}
if !(body is Nothing.Type) {
debugOutput.append(contentsOf: "BODY\n\(body.asJSONString() ?? "No Body")\n\n")
}
if let responseData = rawData {
debugOutput.append(contentsOf: "RESPONSE\n\(String(data: responseData, encoding: .utf8) ?? "No Response Content")")
}
Logging.client.record(debugOutput, domain: .network, level: .debug)
guard let httpResponse = response as? HTTPURLResponse else {
guard error == nil else {
completion?(.failure(nil, NetworkError(reason: .unwrappingResponse)), nil)
return
}
completion?(.failure(nil, NetworkError(reason: .invalidResponseType)), nil)
return
}
let statusCode = httpResponse.statusCode
// We have an error, return it
guard error == nil, NetworkManager.successStatusRange.contains(statusCode) else {
var output: Any?
if let data = rawData {
output = (try? JSONSerialization.jsonObject(with: data,
options: .allowFragments)) ?? "Unable to connect"
Logging.client.record("Response: \(String(data: data, encoding: .utf8) ?? "No error data")", domain: .network)
}
completion?(.failure(statusCode, NetworkError(reason: .requestFailed, json: output)), nil)
return
}
// Safely cast the responseType we are expecting
guard let responseType = responseType else {
completion?(.failure(statusCode, NetworkError(reason: .castingToExpectedType)), nil)
return
}
// If we are expecting nothing, return now (since we will have nothing!)
if responseType is Nothing.Type {
completion?(.success(statusCode), nil)
return
}
guard let data = rawData else {
assertionFailure("Could not cast data from payload when we passed pre-cast checks")
return
}
// Decode the JSON and cast to our expected response type
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let responseObject = try decoder.decode(responseType, from: data)
completion?(.success(statusCode), responseObject)
return
} catch let error {
let content = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
Logging.client.record("Failed to build codable from JSON: \(String(describing: content))\n\nError: \(error)", domain: .network, level: .error)
assertionFailure("Failed to build codable from JSON: \(error)")
completion?(.failure(statusCode, NetworkError(reason: .castingToExpectedType)), nil)
return
}
}
// Submit our request
task.resume()
}
I was reading this article: https://www.raywenderlich.com/4161005-mvvm-with-combine-tutorial-for-ios on Ray Wenderlich about how to use combine. They have an example where it fetches data from an API but it doesn't handle HTTP status codes. I wanted to add it but so far I'm not able to do so.
According to this answer you could add a tryMap but then XCode starts showing errors like: Generic parameter 'T' could not be inferred.
Below the code:
extension WeatherFetcher: WeatherFetchable {
func weeklyWeatherForecast(
forCity city: String
) -> AnyPublisher<WeeklyForecastResponse, WeatherError> {
return forecast(with: makeWeeklyForecastComponents(withCity: city))
}
private func forecast<T>(
with components: URLComponents
) -> AnyPublisher<T, WeatherError> where T: Decodable {
guard let url = components.url else {
let error = WeatherError.network(description: "Couldn't create URL")
return Fail(error: error).eraseToAnyPublisher()
}
return session.dataTaskPublisher(for: URLRequest(url: url))
.mapError { error in
.network(description: error.localizedDescription)
}
.flatMap(maxPublishers: .max(1)) { pair in
decode(pair.data)
}
.eraseToAnyPublisher()
}
}
And I was trying to add
.tryMap { data, response in
guard let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode else {
switch (response as! HTTPURLResponse).statusCode {
case (400...499):
throw ServiceErrors.internalError((response as! HTTPURLResponse).statusCode)
default:
throw ServiceErrors.serverError((response as! HTTPURLResponse).statusCode)
}
}
return data
}
I think you can simply replace the flatMap block with tryMap. And instead of returning data from tryMap, it should be decoded T. So return data line should be return try JSONDecoder().decode(T.self, from: data)
private func forecast<T>(with components: URLComponents) -> AnyPublisher<T, WeatherError> where T: Decodable {
guard let url = components.url else {
let error = WeatherError.network(description: "Couldn't create URL")
return Fail(error: error).eraseToAnyPublisher()
}
return session.dataTaskPublisher(for: URLRequest(url: url))
.tryMap { data, response in
guard let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode else {
switch (response as! HTTPURLResponse).statusCode {
case (400...499):
throw ServiceErrors.internalError((response as! HTTPURLResponse).statusCode)
default:
throw ServiceErrors.serverError((response as! HTTPURLResponse).statusCode)
}
}
return try JSONDecoder().decode(T.self, from: data)
}
.mapError { error in
WeatherError() // some kind of error
}
.eraseToAnyPublisher()
}
I have two services that are working perfectly independently one is a synchronous call to get shopping-lists and another is an asynchronous call to add shopping-lists. The problem comes when i try to get a shopping-lists just after the add-Shopping-lists call has successfully completed.
The function to get shopping-lists never returns it just hangs after i call it in the closure of the add-Shopping-lists function. What is the best way to make these two calls without promises.
Create ShoppingList
func createURLRequest(with endpoint: String, data: ShoppingList? = nil, httpMethod method: String) -> URLRequest {
guard let accessToken = UserSessionInfo.accessToken else {
fatalError("Nil access token")
}
let urlString = endpoint.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
guard let requestUrl = URLComponents(string: urlString!)?.url else {
fatalError("Nil url")
}
var request = URLRequest(url:requestUrl)
request.httpMethod = method
request.httpBody = try! data?.jsonString()?.data(using: .utf8)
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
return request
}
func createShoppingList(with shoppingList: ShoppingList, completion: #escaping (Bool, Error?) -> Void) {
let serviceURL = environment + Endpoint.createList.rawValue
let request = createURLRequest(with: serviceURL, data: shoppingList, httpMethod: HttpBody.post.rawValue)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: { data, response, error -> Void in
guard let _ = data,
let response = response as? HTTPURLResponse,
(200 ..< 300) ~= response.statusCode,
error == nil else {
completion(false, error)
return
}
completion(true, nil)
})
task.resume()
}
Get shoppingLists
func fetchShoppingLists(with customerId: String) throws -> [ShoppingList]? {
var serviceResponse: [ShoppingList]?
var serviceError: Error?
let serviceURL = environment + Endpoint.getLists.rawValue + customerId
let request = createURLRequest(with: serviceURL, httpMethod: HttpBody.get.rawValue)
let semaphore = DispatchSemaphore(value: 0)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: { data, response, error -> Void in
defer { semaphore.signal() }
guard let data = data, // is there data
let response = response as? HTTPURLResponse, // is there HTTP response
(200 ..< 300) ~= response.statusCode, // is statusCode 2XX
error == nil else { // was there no error, otherwise ...
serviceError = error
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let shoppingList = try decoder.decode([ShoppingList].self, from: data)
serviceResponse = shoppingList
} catch let error {
serviceError = error
}
})
task.resume()
semaphore.wait()
if let error = serviceError {
throw error
}
return serviceResponse
}
Usage of function
func addShoppingList(customerId: String, shoppingList: ShoppingList, completion: #escaping (Bool, Error?) -> Void) {
shoppingListService.createShoppingList(with: shoppingList, completion: { (success, error) in
if success {
self.shoppingListCache.clearCache()
let serviceResponse = try? self.fetchShoppingLists(with: customerId)
if let _ = serviceResponse {
completion(true, nil)
} else {
let fetchListError = NSError().error(description: "Unable to fetch shoppingLists")
completion(false, fetchListError)
}
} else {
completion(false, error)
}
})
}
I would like to call the fetchShoppingLists which is a synchronous call and get new data then call the completion block with success.
This question is predicated on a flawed assumption, that you need this synchronous request.
You suggested that you needed this for testing. This is not true: One uses “expectations” to test asynchronous processes; we don’t suboptimize code for testing purposes.
You also suggested that you want to “stop all processes” until the request is done. Again, this is not true and offers horrible UX and subjects your app to possibly be killed by watchdog process if you do this at the wrong time while on slow network. If, in fact, the UI needs to be blocked while the request is in progress, we usually just throw up a UIActivityIndicatorView (a.k.a. a “spinner”), perhaps on top of a dimming/blurring view over the whole UI to prevent users from interacting with the visible controls, if any.
But, bottom line, I know that synchronous requests feel so intuitive and logical, but it’s invariably the wrong approach.
Anyway, I’d make fetchShoppingLists asynchronous:
func fetchShoppingLists(with customerId: String, completion: #escaping (Result<[ShoppingList], Error>) -> Void) {
var serviceResponse: [ShoppingList]?
let serviceURL = environment + Endpoint.getLists.rawValue + customerId
let request = createURLRequest(with: serviceURL, httpMethod: .get)
let session = URLSession.shared
let task = session.dataTask(with: request) { data, response, error in
guard let data = data, // is there data
let response = response as? HTTPURLResponse, // is there HTTP response
200 ..< 300 ~= response.statusCode, // is statusCode 2XX
error == nil else { // was there no error, otherwise ...
completion(.failure(error ?? ShoppingError.unknownError))
return
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let shoppingList = try decoder.decode([ShoppingList].self, from: data)
completion(.success(shoppingList))
} catch let jsonError {
completion(.failure(jsonError))
}
}
task.resume()
}
And then you just adopt this asynchronous pattern. Note, while I’d use the Result pattern for my completion handler, I left yours as it was to minimize integration issues:
func addShoppingList(customerId: String, shoppingList: ShoppingList, completion: #escaping (Bool, Error?) -> Void) {
shoppingListService.createShoppingList(with: shoppingList) { success, error in
if success {
self.shoppingListCache.clearCache()
self.fetchShoppingLists(with: customerId) { result in
switch result {
case .failure(let error):
completion(false, error)
case .success:
completion(true, nil)
}
}
} else {
completion(false, error)
}
}
}
Now, for example, you suggested you wanted to make fetchShoppingLists synchronous to facilitate testing. You can easily test asynchronous methods with “expectations”:
class MyAppTests: XCTestCase {
func testFetch() {
let exp = expectation(description: "Fetching ShoppingLists")
let customerId = ...
fetchShoppingLists(with: customerId) { result in
if case .failure(_) = result {
XCTFail("Fetch failed")
}
exp.fulfill()
}
waitForExpectations(timeout: 10)
}
}
FWIW, it’s debatable that you should be unit testing the server request/response at all. Often instead mock the network service, or use URLProtocol to mock it behind the scenes.
For more information about asynchronous tests, see Asynchronous Tests and Expectations.
FYI, the above uses a refactored createURLRequest, that uses the enumeration for that last parameter, not a String. The whole idea of enumerations is to make it impossible to pass invalid parameters, so let’s do the rawValue conversion here, rather than in the calling point:
enum HttpMethod: String {
case post = "POST"
case get = "GET"
}
func createURLRequest(with endpoint: String, data: ShoppingList? = nil, httpMethod method: HttpMethod) -> URLRequest {
guard let accessToken = UserSessionInfo.accessToken else {
fatalError("Nil access token")
}
guard
let urlString = endpoint.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let requestUrl = URLComponents(string: urlString)?.url
else {
fatalError("Nil url")
}
var request = URLRequest(url: requestUrl)
request.httpMethod = method.rawValue
request.httpBody = try! data?.jsonString()?.data(using: .utf8)
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
return request
}
I am sure it could be alot better, but this is my 5 minute version.
import Foundation
import UIKit
struct Todo: Codable {
let userId: Int
let id: Int
let title: String
let completed: Bool
}
enum TodoError: String, Error {
case networkError
case invalidUrl
case noData
case other
case serializationError
}
class TodoRequest {
let todoUrl = URL(string: "https://jsonplaceholder.typicode.com/todos")
var todos: [Todo] = []
var responseError: TodoError?
func loadTodos() {
var responseData: Data?
guard let url = todoUrl else { return }
let group = DispatchGroup()
let task = URLSession.shared.dataTask(with: url) { [weak self](data, response, error) in
responseData = data
self?.responseError = error != nil ? .noData : nil
group.leave()
}
group.enter()
task.resume()
group.wait()
guard responseError == nil else { return }
guard let data = responseData else { return }
do {
todos = try JSONDecoder().decode([Todo].self, from: data)
} catch {
responseError = .serializationError
}
}
func retrieveTodo(with id: Int, completion: #escaping (_ todo: Todo? , _ error: TodoError?) -> Void) {
guard var url = todoUrl else { return }
url.appendPathComponent("\(id)")
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let todoData = data else { return completion(nil, .noData) }
do {
let todo = try JSONDecoder().decode(Todo.self, from: todoData)
completion(todo, nil)
} catch {
completion(nil, .serializationError)
}
}
task.resume()
}
}
class TodoViewController: UIViewController {
let request = TodoRequest()
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.global(qos: .background).async { [weak self] in
self?.request.loadTodos()
self?.request.retrieveTodo(with: 1, completion: { [weak self](todoData, error) in
guard let strongSelf = self else { return }
if let todoError = error {
return debugPrint(todoError.localizedDescription)
}
guard let todo = todoData else {
return debugPrint("No todo")
}
debugPrint(strongSelf.request.todos)
debugPrint(todo)
})
}
}
}