How to make the right API call? - swift

I am trying to access fixer.io by making an API call. It is the first time than I am trying to do so, but I don't get the result wanted. I would like to get the "rate" and the "result" from this JSON file.
{
"success": true,
"query": {
"from": "GBP",
"to": "JPY",
"amount": 25
},
"info": {
"timestamp": 1519328414,
"rate": 148.972231
},
"historical": ""
"date": "2018-02-22"
"result": 3724.305775
}
The method that I have implemented is this one, but I can not figure out how to retrieve "rate" and "result" when making this API call.
extension APIsRuler {
func getExchangeRate(from: String, to: String, amount: String, callback: #escaping (Bool, ConversionResult?) -> Void) {
var request = URLRequest(url: APIsRuler.exchangeURL)
let body = "convert?access_key=\(APIsRuler.exchangeAPI)&from=\(from)&to=\(to)&amount=\(amount)"
request.httpMethod = "GET"
request.httpBody = body.data(using: .utf8)
let session = URLSession(configuration: .default)
task?.cancel()
task = session.dataTask(with: request) { (data, response, error) in
DispatchQueue.main.async {
guard let data = data, error == nil else {
return callback(false, nil)
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
return callback(false, nil)
}
guard let responseJSON = try? JSONDecoder().decode([String: Double].self, from: data),
let rate = responseJSON["rate"],
let result = responseJSON["result"] else {
return callback(false, nil)
}
let conversionResult = ConversionResult(exchangeRate: rate, exchangeResult: result)
callback(true, conversionResult)
}
}
task?.resume()
}
}

Use a real model object, like this:
struct Conversion: Codable {
let success: Bool
let query: Query
let info: Info
let historical, date: String
let result: Double
}
struct Info: Codable {
let timestamp: Int
let rate: Double
}
struct Query: Codable {
let from, to: String
let amount: Int
}
and parse your response into it using JSONDecoder:
do {
let conversion = try JSONDecoder().decode(Conversion.self, from: data)
let rate = conversion.info.rate
let result = conversion.result
} catch { print(error) }

You are mixing up two different APIs.
Either use JSONSerialization, the result is a dictionary and you get the values by key and index subscription. And you have to downcast every type and consider the nested rate value.
guard let responseJSON = try? JSONSerialization.jsonObject(with: data) as? [String:Any],
let info = responseJSON["info"] as? [String:Any],
let rate = info["rate"] as? Double,
let result = responseJSON["result"] as? Double else {
return callback(false, nil)
}
let conversionResult = ConversionResult(exchangeRate: rate, exchangeResult: result)
callback(true, conversionResult)
Or use JSONDecoder then you have to create structs, decoding to [String:Double] can work only if all values in the root object are Double which is clearly not the case.
struct Root: Decodable {
let info: Info
let result: Double
}
struct Info: Decodable {
let rate: Double
}
guard let responseJSON = try? JSONDecoder().decode(Root.self, from: data) else {
return callback(false, nil)
}
let conversionResult = ConversionResult(exchangeRate: responseJSON.info.rate, exchangeResult: responseJSON.result)
callback(true, conversionResult)
The code is only an example to keep your syntax. Practically you are strongly discouraged from using try? when decoding JSON. Always catch and handle errors
do {
let responseJSON = try JSONDecoder().decode(Root.self, from: data)
let conversionResult = ConversionResult(exchangeRate: responseJSON.info.rate, exchangeResult: responseJSON.result)
callback(true, conversionResult)
} catch {
print(error)
return callback(false, nil)
}

Related

Does Swift task run first or print() first when I tap my UIButton?

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).

How to use a value from a struct dictionary and convert it to another type?

Here I want to be able to use the value returned from an array. It returns as a type from a struct. I'm unsure of how to use the value as an integer.
struct Item: Codable {
let data: [String : Datum]
}
struct Datum: Codable {
let value: Int
}
var array = Item(data: ["1" : Datum(value: 1),"2": Datum(value: 2), "3":Datum(value: 3)])
var keyArray = ["1", "2", "3"]
print(array.data[keyArray[0]]!)
// Prints Datum(value: 1)
print(array.data[keyArray[0]]! + 1)
//This produces an error "Cannot convert value of type 'Datum' to expected argument type 'Int'"
//Expected result should be 2
My use case is when I get returned a decoded JSON it normally comes back as a dictionary. I'm wanting to use the values returned with a key but I feel like I'm one step short.
Context
Full JSON Link
I'm going to retrieve values from this JSON. (Example from large JSON file)
{"data":{"2":{"high":179,"highTime":1628182107,"low":177,"lowTime":1628182102},"6":{"high":189987,"highTime":1628179815,"low":184107,"lowTime":1628182100},"8":{"high":190800,"highTime":1628181435,"low":188100,"lowTime":1628182095}
}}
The string in front refers to an item ID.
The struct that I came up to decode goes like this.
// MARK: - Single
struct Single: Codable {
let data: [String: Datum]
}
// MARK: - Datum
struct Datum: Codable {
let high, highTime: Int
let low, lowTime: Int?
}
From there I'm planning to iterate through the JSON response to retrieve the item prices I'd want.
#available(iOS 15.0, *)
struct ContentView: View {
#State var dataFromURL: Single = Single(data: [:])
var body: some View {
VStack {
Text("Hello, world!")
.padding()
}
.onAppear {
async {
try await decode()
}
}
}
func decode() async throws -> Single {
let decoder = JSONDecoder()
let urlString = "https://prices.runescape.wiki/api/v1/osrs/latest"
guard let url = URL(string: urlString) else { throw APIError.invalidURL }
let (data, response) = try await URLSession.shared.data(from: url)
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { throw APIError.invalidServerResponse }
guard let result = try? decoder.decode(Single.self, from: data) else { throw APIError.invalidData }
//We copy our result to an existing variable
dataFromURL = result
return result
}
}
enum APIError: Error {
case invalidURL
case invalidServerResponse
case invalidData
}
extension APIError: CustomStringConvertible {
public var description: String {
switch self {
case.invalidURL:
return "Bad URL"
case .invalidServerResponse:
return "The server did not return 200"
case .invalidData:
return "Their server returned bad data"
}
}
}
I haven't gotten further than grabbing the response from the URL. That is why once I start manipulating the data I'd like to use the response to find other things like what would a profit/loss with another item become. Which isn't the goal of this question at the moment.
The object model to parse that JSON would be:
struct Price: Decodable {
let high: Int?
let highTime: Date?
let low: Int?
let lowTime: Date?
}
struct ResponseObject: Decodable {
let prices: [String: Price]
enum CodingKeys: String, CodingKey {
case prices = "data"
}
}
(Note, the documentation says that either high or low might be missing, so we have to make them all optionals.)
Now, the id number is being passed as a string in the JSON/ResponseObject. But that is a number (look at mapping). So, I would remap that dictionary so that the key was an integer, e.g.
enum ApiError: Error {
case unknownError(Data?, URLResponse?)
}
func fetchLatestPrices(completion: #escaping (Result<[Int: Price], Error>) -> Void) {
let url = URL(string: "https://prices.runescape.wiki/api/v1/osrs/latest")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
error == nil,
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {
completion(.failure(error ?? ApiError.unknownError(data, response)))
return
}
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
do {
let responseObject = try decoder.decode(ResponseObject.self, from: responseData)
let keysAndValues = responseObject.prices.map { (Int($0.key)!, $0.value) }
let prices = Dictionary(uniqueKeysWithValues: keysAndValues)
completion(.success(prices))
} catch {
completion(.failure(error))
}
}
task.resume()
}
The code that converts that [String: Price] to a [Int: Price] is this:
let keysAndValues = responseObject.prices.map { (Int($0.key)!, $0.value) }
let prices = Dictionary(uniqueKeysWithValues: keysAndValues)
I must say that this is a questionable API design, to have keys returned as integers in one endpoint and as strings as another. But it is what it is. So, the above is how you handle that.
Anyway, now that you have a dictionary of prices, keyed by the id numbers, you can use that in your code, e.g.
var prices: [Int: Price] = [:]
var products: [Product] = []
let group = DispatchGroup()
group.enter()
fetchLatestPrices { result in
defer { group.leave() }
switch result {
case .failure(let error):
print(error)
case .success(let values):
prices = values
}
}
group.enter()
fetchProducts { result in
defer { group.leave() }
switch result {
case .failure(let error):
print(error)
case .success(let values):
products = values }
}
group.notify(queue: .main) {
for product in products {
print(product.name, prices[product.id] ?? "no price found")
}
}
Where
func fetchProducts(completion: #escaping (Result<[Product], Error>) -> Void) {
let url = URL(string: "https://prices.runescape.wiki/api/v1/osrs/mapping")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
error == nil,
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {
completion(.failure(error ?? ApiError.unknownError(data, response)))
return
}
do {
let products = try JSONDecoder().decode([Product].self, from: responseData)
completion(.success(products))
} catch {
completion(.failure(error))
}
}
task.resume()
}
And
struct Product: Decodable {
let id: Int
let name: String
let examine: String
let members: Bool
let lowalch: Int?
let limit: Int?
let value: Int
let highalch: Int?
let icon: String
}
(As an aside, I do not know if some of these other properties should be optionals or not. I just used optionals where I empirically discovered that they are occasionally missing.)

Swift: getting nil when decoding API response

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

Swift: Multithreading Issue Getting Data From API

I'm having a problem where sometimes I can't successfully decode a json file due to a slow networking call. I have a list of stocks and I'm trying to successfully decode each one before the next stock gets decoded.
For example, I would have a list of 4 stocks but 3 would be successful decoded but 1 won't. The one that fails is also random. When I print out the url for the one that fails, the url and json file is correct yet I get an error of it not reading because its on a wrong format.
The list of stocks are retrieved through Firebase and after I receive them, I have a completion handler that tries to make a network call to the server. The reason why I added Firestore code here is because when I put a stop point at when case is successful, I notice that its hitting Thread 5 out of 14 Threads. Is having this many threads common? I know it's a threading issue but am having such a huge problem identifying where I should do dispatchGroups. Any help and clarifications would be much appreciated!
APIManager
private var stocks = [Stock]()
func getStockList( for symbols: [Stock], completion: ((Result<[Stock]>) -> Void)?) {
let dispatchGroup = DispatchGroup()
for symbol in symbols {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = APIManager.baseAPIURL
urlComponents.path = "\(APIManager.baseRelativePath)/market/batch"
//URL parameters
let token = URLQueryItem(name: "token", value: "fakeToken")
let symbolsItem = URLQueryItem(name: "symbols", value: symbol.symbol)
let typesItem = URLQueryItem(name: "types", value: "quote")
urlComponents.queryItems = [token, symbolsItem, typesItem]
guard let url = urlComponents.url else { fatalError("Could not create URL from components") }
var request = URLRequest(url: url)
request.httpMethod = "GET"
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
dispatchGroup.enter()
let task = session.dataTask(with: request) { (responseData, response, err) in
guard err == nil else {
completion?(.failure(err!))
return
}
guard let jsonData = responseData else {
let err = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "Data was not retrieved from request"]) as Error
completion?(.failure(err))
return
}
let decoder = JSONDecoder()
do {
let data = try decoder.decode([String: Stock].self, from: jsonData)
let jsonData = data.map{ $0.value }
completion?(.success(jsonData))
dispatchGroup.leave()
} catch {
completion?(.failure(error))
print("Failed to decode using stock URL for \(symbol.symbol ?? ""): \n \(url)")
}
}
task.resume()
}
}
HomeViewController
class HomeViewController: UIViewController, LoginViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
fetchStocksFromFireStore(completion: { (stocks) -> Void in
APIManager.shareInstance.getStockList(for: self.fetchedStocks) { (result) in
switch result {
case .success(let stocks):
stocks.forEach { (stock) in
print("Company: \(stock.companyName ?? "")")
}
case .failure(let error):
print(error.localizedDescription)
}
}
self.tableView.reloadData()
})
}
}
func fetchStocksFromFireStore(completion: #escaping ([Stock]) -> ()) {
let uid = Auth.auth().currentUser?.uid ?? ""
let db = Firestore.firestore()
db.collection("users").document(uid).collection("stocks").getDocuments { (snapshot, err) in
if let err = err {
print("Error getting stocks snapshot", err.localizedDescription)
return
} else {
snapshot?.documents.forEach({ (snapshot) in
var stock = Stock()
stock.symbol = snapshot.documentID
self.fetchedStocks.append(stock)
})
completion(self.fetchedStocks)
}
}
}
Model
struct Stock {
var symbol: String?
var companyName: String?
var latestPrice: Double?
enum CodingKeys: String, CodingKey {
case symbol
case companyName
case latestPrice
}
enum QuoteKeys: String, CodingKey {
case quote
}
}
extension Stock: Encodable {
func encode(to encoder: Encoder) throws {
var quoteContainer = encoder.container(keyedBy: QuoteKeys.self)
var quoteNestedValues = quoteContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .quote)
try quoteNestedValues.encode(symbol, forKey: .symbol)
try quoteNestedValues.encode(companyName, forKey: .companyName)
try quoteNestedValues.encode(latestPrice, forKey: .latestPrice)
}
}
extension Stock: Decodable {
init(from decoder: Decoder) throws {
let quoteValues = try decoder.container(keyedBy: QuoteKeys.self)
let quoteNestedValues = try quoteValues.nestedContainer(keyedBy: CodingKeys.self, forKey: .quote)
symbol = try quoteNestedValues.decode(String.self, forKey: .symbol)
companyName = try quoteNestedValues.decode(String.self, forKey: .companyName)
latestPrice = try quoteNestedValues.decode(Double.self, forKey: .latestPrice)
}
}

How can i make an http get request with dictionary as parameter in swift

Here is the API Call how can I pass a dictionary as the parameter to the URL which is a get request, Don't want to use Alamofire in this.
The URL is - http://mapi.trycatchtech.com/v1/naamkaran/post_list_by_cat_and_gender_and_page?category_id=3&gender=1&page=1
func getDisplayJsonData(url: String, parameters: [String: Any], completion: #escaping displayCompletionHandler) {
guard let url = URL(string: url) else {return}
URLSession.shared.dataTask(with: url) { (data, response, error) in
if error != nil {
debugPrint(error?.localizedDescription ?? "")
completion(nil)
return
} else {
guard let data = data else {return completion(nil)}
let decoder = JSONDecoder()
do{
let displayJson = try decoder.decode(Display.self, from: data)
completion(displayJson)
} catch{
debugPrint(error.localizedDescription)
completion(nil)
return
}
}
} .resume()
}
}
Where I am calling this function here and passing the dictionary values.
extension DisplayVC {
func getData() {
let params = ["category_id": categoryId, "gender": genderValue, "page": pageNumber] as [String : Any]
DisplayService.instance.getDisplayJsonData(url: BASE_URL, parameters: params) { (receivedData) in
print(receivedData)
}
}
}
SWIFT 4 / Xcode 10
If this the only case, a simple solution is:
func getDisplayJsonData(url: String, parameters: [String: Any], completion: #escaping displayCompletionHandler) {
var urlBase = URLComponents(string: url)
guard let catValue = parameters["category_id"] as? Int else {return}
guard let genderValue = parameters["gender"] as? Int else {return}
guard let pageValue = parameters["page"] as? Int else {return}
urlBase?.queryItems = [URLQueryItem(name: "category_id", value: String(catValue)), URLQueryItem(name: "gender", value: String(genderValue)), URLQueryItem(name: "page", value: String(pageValue))]
//print(urlBase?.url)
guard let urlSafe = urlBase?.url else {return}
URLSession.shared.dataTask(with: urlSafe) { (data, response, error) in
//Your closure
}.resume()
}