Swift: Multithreading Issue Getting Data From API - swift

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

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 can I init struct: Codable without default value

I want to get Json from API, and use Codable protocol, but when I init published var, I get an error.
struct Search: Codable {
let result: [String]
}
class SearchViewModel: ObservableObject {
#Published var data = Search()
func loadData(search: String) {
var urlComps = URLComponents(string: getUrl)
let queryItems = [URLQueryItem(name: "result", value: search)]
urlComps!.queryItems = queryItems
let url = urlComps!.url!.absoluteString
guard let Url = URL(string: url) else { return }
URLSession.shared.dataTask(with: Url) { (data, res, err) in
do {
if let data = data {
let decoder = JSONDecoder()
let result = try decoder.decode(Search.self, from: data)
DispatchQueue.main.async {
self.data = result
}
} else {
print("there's no Data😭")
}
} catch (let error) {
print("Error!")
print(error.localizedDescription)
}
}.resume()
}
}
Change
#Published var data:Search?

How to get specific properties from a JSON object from HTTP request using URLSession

With the following code I'm able to effectively get a JSON object, what I'm not sure how to do is retrieve the specific properties from the object.
Swift Code
#IBAction func testing(_ sender: Any) {
let url = URL(string: "http://example.com/cars/mustang")
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard error == nil else {
print(error!)
return
}
guard let data = data else {
print("Data is empty")
return
}
let json = try! JSONSerialization.jsonObject(with: data, options: [])
print(json)
}
task.resume()
}
Here is what I see when I run the above code...
Output - JSON Object
(
{
color = "red";
engine = "5.0";
}
)
How can I get just the property color?
Thanks
Create a class which confirm the decodable protocol; CarInfo for example in your case
class CarInfo: Decodable
Create attributes of the class
var color: String
var engine: String
Create JSON key enum which inherits from CodingKey
enum CarInfoCodingKey: String, CodingKey {
case color
case engine
}
implement init
required init(from decoder: Decoder) throws
the class will be
class CarInfo: Decodable {
var color: String
var engine: String
enum CarInfoCodingKey: String, CodingKey {
case color
case engine
}
public init(from decoder: Decodabler) throws {
let container = try decoder.container(keyedBy: CarInfoCodingKey.self)
self.color = try container.decode(String.self, forKey: .color)
self.engine = try contaire.decode(String.self, forKey: .engine)
}
}
call decoder
let carinfo = try JsonDecoder().decode(CarInfo.self, from: jsonData)
Here is how I did it...
#IBAction func testing(_ sender: Any) {
let url = URL(string: "http://example.com/cars/mustang")
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard error == nil else {
print(error!)
return
}
guard let data = data else {
print("Data is empty")
return
}
let json = try! JSONSerialization.jsonObject(with: data, options: [])
guard let jsonArray = json as? [[String: String]] else {
return
}
let mustangColor = jsonArray[0]["color"]
print(mustangColor!)
}
task.resume()
}

Swift 4 Codable - API provides sometimes an Int sometimes a String

I have Codables running now. But the API has some String entries that can sometimes have an Int value of 0 if they are empty. I was searching here and found this: Swift 4 Codable - Bool or String values But I'm not able to get it running
My struct
struct check : Codable {
let test : Int
let rating : String?
}
Rating is most of the time something like "1Star". But if there is no rating I get 0 as Int back.
I'm parsing the data like this:
enum Result<Value> {
case success(Value)
case failure(Error)
}
func checkStar(for userId: Int, completion: ((Result<check>) -> Void)?) {
var urlComponents = URLComponents()
urlComponents.scheme = "https"
urlComponents.host = "xyz.com"
urlComponents.path = "/api/stars"
let userIdItem = URLQueryItem(name: "userId", value: "\(userId)")
urlComponents.queryItems = [userIdItem]
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
config.httpAdditionalHeaders = [
"Authorization": "Bearer \(keytoken)"
]
let session = URLSession(configuration: config)
let task = session.dataTask(with: request) { (responseData, response, responseError) in
DispatchQueue.main.async {
if let error = responseError {
completion?(.failure(error))
} else if let jsonData = responseData {
// Now we have jsonData, Data representation of the JSON returned to us
// from our URLRequest...
// Create an instance of JSONDecoder to decode the JSON data to our
// Codable struct
let decoder = JSONDecoder()
do {
// We would use Post.self for JSON representing a single Post
// object, and [Post].self for JSON representing an array of
// Post objects
let posts = try decoder.decode(check.self, from: jsonData)
completion?(.success(posts))
} catch {
completion?(.failure(error))
}
} else {
let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey : "Data was not retrieved from request"]) as Error
completion?(.failure(error))
}
}
}
task.resume()
}
Loading it:
func loadStars() {
checkStar(for: 1) { (result) in
switch result {
case .success(let goo):
dump(goo)
case .failure(let error):
fatalError(error.localizedDescription)
}
}
}
I hope someone can help me there, cause I'm not completely sure how this parsing, etc. works.
you may implement your own decode init method, get each class property from decode container, during this section, make your logic dealing with wether "rating" is an Int or String, sign all required class properties at last.
here is a simple demo i made:
class Demo: Decodable {
var test = 0
var rating: String?
enum CodingKeys: String, CodingKey {
case test
case rating
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let test = try container.decode(Int.self, forKey: .test)
let ratingString = try? container.decode(String.self, forKey: .rating)
let ratingInt = try? container.decode(Int.self, forKey: .rating)
self.rating = ratingString ?? (ratingInt == 0 ? "rating is nil or 0" : "rating is integer but not 0")
self.test = test
}
}
let jsonDecoder = JSONDecoder()
let result = try! jsonDecoder.decode(Demo.self, from: YOUR-JSON-DATA)
if rating API's value is normal string, you will get it as you wish.
if rating API's value is 0, rating will equal to "rating is nil or 0"
if rating API's value is other integers, rating will be "rating is integer but not 0"
you may modify decoded "rating" result, that should be easy.
hope this could give you a little help. :)
for more info: Apple's encoding and decoding doc

Is there anyway to access the Firebase Topics a user is subscribed to?

I'm currently trying to migrate my app to firebase and I'm looking for the Firebase equivalent of Parse Installations and Channels.
What I've found is that we are supposed to use topics however in my app "subscribing" and "unsubscribing" to topics is common yet there is no way (that I have found) to see what topics a user is subscribed to. Any ideas?
I've looked through the Firebase documentation but I'm new to Firebase so maybe someone with more experience would know: https://firebase.google.com/docs/cloud-messaging/android/topic-messaging#managing_topic_subscriptions_from_the_server
Thanks in advance for any help!
FCM topic subscriptions are based on an application's Instance ID, so when you subscribe or unsubscribe to or from a topic the IID is used.
You can use the Instance ID API to get information about a particular IID, this information includes the topics that the IID is currently subscribed to. See the reference
You have to make a request to the Instance ID API. I created an extension
https://gist.github.com/eduardo22i/0ee8960c36659b17fbc538964e929cac
extension Messaging {
static private let accessToken = "" //Server Web Key
struct Topic : Decodable {
var name : String?
var addDate : String?
}
struct Rel : Decodable {
var topics = [Topic]()
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let relContainer = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .rel)
let topicsContainer = try relContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .topics )
for key in topicsContainer.allKeys {
var topic = Topic()
topic.name = key.stringValue
let topicContainer = try? topicsContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: key)
topic.addDate = try! topicContainer?.decode(String.self, forKey: .addDate)
self.topics.append(topic)
}
}
struct CodingKeys : CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
static let rel = CodingKeys(stringValue: "rel")!
static let topics = CodingKeys(stringValue: "topics")!
static let addDate = CodingKeys(stringValue: "addDate")!
}
}
func loadTopics(block : #escaping (_ topics: [Messaging.Topic]?, _ error: Error?) -> Void ) {
if let token = InstanceID.instanceID().token() {
let url = URL(string: "https://iid.googleapis.com/iid/info/\(token)?details=true")!
var request = URLRequest(url: url)
request.addValue("key=\(Messaging.accessToken)", forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
DataManager.shared.make(request: urlRequest, block: { (data, error) in
if let data = data {
let decoder = JSONDecoder()
let rel = try? decoder.decode(Rel.self, from: data)
block(rel?.topics, error)
} else {
block(nil, error)
}
}
dataTask.resume()
}
}
}
// Usage
Messaging.messaging().loadTopics { (topics, error) in
topics?.forEach({ (topic) in
print(topic.name)
})
}
Here is the update from #Eduardo's, requested by #Nico
2 things:
You need to import Firebase, since you'll need the InstanceID and the messaging module.
Make sure you fill out the API key (accessToken), which can be found under:
(gear-next-to-project-name) > Project Settings > Cloud Messaging -> Server Key is the API key.
This is the updated code:
import Firebase
// Grab susbcribed channels
// Source 1: https://stackoverflow.com/questions/38948720/is-there-anyway-to-access-the-firebase-topics-a-user-is-subscribed-to
// Source 2: https://developers.google.com/instance-id/reference/server#get_information_about_app_instances
extension Messaging {
// Needs to be grabbed from: https://stackoverflow.com/questions/37337512/where-can-i-find-the-api-key-for-firebase-cloud-messaging
static private let accessToken = ""
struct Topic : Decodable {
var name : String?
var addDate : String?
}
struct Rel : Decodable {
var topics = [Topic]()
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let relContainer = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .rel)
let topicsContainer = try relContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .topics )
for key in topicsContainer.allKeys {
var topic = Topic()
topic.name = key.stringValue
let topicContainer = try? topicsContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: key)
topic.addDate = try! topicContainer?.decode(String.self, forKey: .addDate)
self.topics.append(topic)
}
}
struct CodingKeys : CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
static let rel = CodingKeys(stringValue: "rel")!
static let topics = CodingKeys(stringValue: "topics")!
static let addDate = CodingKeys(stringValue: "addDate")!
}
}
func loadTopics(block : #escaping (_ topics: [Messaging.Topic]?, _ error: Error?) -> Void ) {
InstanceID.instanceID().instanceID { (result, error) in
if let err = error {
block(nil, err)
} else if let result = result {
let url = URL(string: "https://iid.googleapis.com/iid/info/\(result.token)?details=true")!
var request = URLRequest(url: url)
request.addValue("key=\(Messaging.accessToken)", forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let data = data {
let decoder = JSONDecoder()
let rel = try? decoder.decode(Rel.self, from: data)
block(rel?.topics, error)
} else {
block(nil, error)
}
}
dataTask.resume()
}
}
}
}
Usage is like this:
Messaging.messaging().loadTopics { (topics, error) in
topics?.forEach({ (topic) in
print("Subscribed to topic: \(topic.name ?? "No name")")
})
}