Property is coming up empty when called from another class when using URLSession - ios

For some reason, the products array is coming back empty when I try and access it from another class. What am I doing wrong, and how can I get the products array to populate? Is it something related to the do/catch?
The print statement shown will give me what I'm looking for, but when I try and use the property in another class after the retrieve method has been called, it comes up empty.
For information, "Product" is a struct that has name, description, etc properties attached.
private let productListUrl = URL(string: "https://api/products.json")
var products = [Product]()
func retrieveProductList() {
if let productListUrl = productListUrl {
URLSession.shared.dataTask(with: productListUrl) { (data, response, error) in
if let data = data {
do {
let jsonData = try JSONSerialization.jsonObject(with: data, options: []) as! [String:Any]
let tempArray: Array = jsonData["products"] as! [Any]
for product in tempArray {
let newProduct = Product(json: product as! [String : Any])
self.products.append(newProduct!)
}
print("In ProductService: \(self.products)")
}
catch {
print("An error occured while attempting to read data")
}
}
}.resume()
}
}

As maddy noted, this is because the URL call is asynchronous.
You basically have 3 options:
Use a semaphore approach and make your retrieveProductList method synchronous.
Change your class to have a delegate property that you can ping when the URL request finishes.
Add a completion handler to your retrieveProductList method that is called when the URL request finishes.
I personally would lean towards option 3:
func retrieveProductList(completion: #escaping ([Product])->())
{
// Right after you print the products...
completion(self.products)
}

Related

Swift 5 : Escaping closure captures 'inout' parameter

I already have the response data that I received from the server. This response data have some bakers data.
Now I want to calculate the distance of the user and bakery and then store it in the same modal class. I have created a function for it. And as this function need to be used in 4,5 view controllers, my plan is to create as an extension of UIViewController
func getDistanceUserBakery(bakeryData : inout [BakeryRecord], completion : #escaping (Int?) -> () ) {
for index in 0...(bakeryData.count-1) {
//1
let googleApiAdd = "https://maps.googleapis.com/maps/api/distancematrix/json?units=imperial&"
//2
let origin = "origins=\(UserLocation.coordinates.latitude),\(UserLocation.coordinates.longitude)"
//3
let destination = "&destinations=\(bakeryData[index].location?.coordinates?[1] ?? 0.0),\(bakeryData[index].location?.coordinates?[0] ?? 0.0)"
//4
let googleKey = "&key=\(GOOGLE_KEY)"
//5
let url = googleApiAdd + origin + destination + googleKey
let request = URLRequest(url: URL(string: url)!)
//6 - this line is showing the error.
let task = URLSession.shared.dataTask(with: request) {(data, response, error) in
guard let data = data else {
completion(nil)
Toast.show(message: "Unable to calculate distance from user to bakery", controller: self)
return }
let stringResponse = String(data: data, encoding: .utf8)!
let dictData = stringResponse.convertToDictionary()
do {
let jsonData = try JSONSerialization.data(withJSONObject: dictData as Any, options: .prettyPrinted)
let decoder = JSONDecoder()
let model = try decoder.decode(GoogleDistance.self, from: jsonData)
bakeryData[index].disanceInMiles = model.rows?[0].elements?[0].distance?.text ?? "NaN"
completion(index)
} catch let parsingError {
print("Error data :", parsingError)
completion(nil)
}
}
task.resume()
}
This is how I call this function once I have received the data from my server,
self.getDistanceUserBakery(bakeryData: &self.bakeryData) { index in
if index != nil {
DispatchQueue.main.async {
// here I am thinking as the bakeryData will hold the new value for distanceInMiles, the collectionView will start showing up that result on reload.
self.resultCollection.reloadItems(at: [IndexPath(item: index!, section: 0)])
}
}
}
Now the Question:
As I know, when you pass parameters as inout, there values can be changed from inside your function, and those changes reflect in the original value outside the function.
But when I try the code , it says Escaping closure captures 'inout' parameter 'bakeryData'. In my code , //6 is producing the error.
How to fix this error?
As #Paulw11 suggested in comments,
Is BakeryData a struct? If so then simply make it a class. If you make
BakerData a class then the array contains reference types and you can
update the element's properties
I changed the struct to class and it did work.

How to parse json data that provides another url with more data

I'm a little confused how would I parse a json API that gives me 20 objects but then gives me a key of "next" having a url that gives me another 20 objects. I'm using this Pokemon API. It gives me 4 keys: count, previous, results and next. I'm trying to display them all in a collection view but not all at the same time. I would like to load more when the collection view is scrolling down.
I'm just trying to get the name at the moment. This is how my code looks like.
I get it to load the first 20 Pokemon in the collection view. However I don't know how to load the next 20 Pokemon or the 20 after. This is how the json file looks like if the link didn't work.
I would appreciate any help given. :)
You can try using a recursive function reusing the loadPokemonsData function something like this:
func loadPokemonsData(url: String, quantity: Int?) {
let request = URLRequest(url: URL(string: url)!)
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if error != nil {
print(error!)
}
do {
let jsonResults = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as! NSDictionary
let pokemonArray = jsonResults.value(forKey: "results") as! [[String: Any]]
var isPokemonsEqualsToQuantity: Bool = false
for pokemonData in pokemonArray {
if let quantity = quantity {
guard self.pokemons.count < quantity else {
isPokemonsEqualsToQuantity = true
break
}
}
guard let name = pokemonData["name"] as? String else {
return
}
self.pokemon = Pokemon(name: name)
self.pokemons.append(self.pokemon)
}
guard let nextURL = jsonResults.value(forKey: "next") as? String, !isPokemonsEqualsToQuantity else {
for pokemon in self.pokemons {
print(pokemon.name)
}
print(self.pokemons.count)
return
}
self.loadPokemonsData(url: nextURL, quantity: quantity)
} catch let err as NSError {
print(err.localizedDescription)
}
}
task.resume()
}
Attach a screen of algorithm function running... it prints 791 pokemons.
Hope it helps you!
EDITED
Next time you ask put your code please... it will be easier help you!.
I've updated the code to set the quantity you want (nil if you want to get all pokemons), Therefore it will only get the pokemons in the order API returns it, if you want a specific pokemons from ALL pokemons you may do a sort after obtaining all pokemons.

How do I get a value from an NSURLSession task into an instance variable?

I have a tableView which I want to fill with a list of items provided by a web service. The service returns a JSON object with status (success or failure) and shows (an array of strings).
In viewDidLoad I call the custom method getShowsFromService()
func getShowsFromService() {
// Send user data to server side
let myURL = NSURL(string: "https://myurl.com/srvc/shows.php")
// Create session instance
let session = NSURLSession.sharedSession()
var json:NSDictionary = [:]
// Create the task
let task = session.dataTaskWithURL(myURL!) { //.dataTaskWithRequest(request) {
(data, response, error) in
guard let data = data else {
print("Error: \(error!.code)")
print("\(error!.localizedDescription)")
return
}
do {
json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as! NSDictionary
} catch {
print (error)
}
let sts = json["status"] as! NSString
print("\(sts)")
}
// Resume the task so it starts
task.resume()
let shows = json["shows"] as! NSArray
for show in shows {
let thisshow = show as! String
showsArray.append(thisshow)
}
// Here I get "fatal error: unexpectedly found nil while unwrapping an Optional value"
}
The method receives the JSON object and puts it into a dictionary. Then I want to use that dictionary to call json['shows'] in order to get to the array of shows which I want to store in an instance variable called showsArray. The idea is to use showsArray in tableView(cellForRowAtIndexPath) in order to fill in the data.
The problem is that I can't get the Dictionary into the variable. If I try to do it inside the task, I get an error that says I need to call self.showsArray and if I do, the data doesn't go inside the array. If I do it outside the task I get an error because it says I'm trying to force unwrap a nil value.
How can I get the Dictionary created within the task out into the showsArray var?
The dataTaskWithURL method makes an async call, so as soon as you do task.resume() it will jump to the next line, and json["shows"] will return nil as the dictionary is empty at this point.
I would recommend moving that logic to a completion handler somewhere in your class. Something along the lines of:
func getShowsFromService() {
let myURL = NSURL(string: "https://myurl.com/srvc/shows.php")
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(myURL!, completionHandler: handleResult)
task.resume()
}
//-handle your result
func handleResult(data: NSData?, response: NSURLResponse?, error: NSError?) {
guard let data = data else {
print("Error: \(error!.code)")
print("\(error!.localizedDescription)")
return
}
do {
if let json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions()) as! NSDictionary {
if let shows = json["shows"] as! NSArray {
//- this is still in a separate thread
//- lets go back to the main thread!
dispatch_async(dispatch_get_main_queue(), {
//- this happens in the main thread
for show in shows {
showsArray.append(show as! String)
}
//- When we've got our data ready, reload the table
self.MyTableView.reloadData()
self.refreshControl?.endRefreshing()
});
}
}
} catch {
print (error)
}
}
The snippet above should serve as a guide (I dont have access to a playground atm).
Note the following:
as soon as the task completes (asynchronously -> different thread) it will call the new function handleResult which will check for errors and if not, it will use the dispatcher to perform your task on the main thread. I'm assuming showsArrays is a class property.
I hope this helps
EDIT:
As soon as you fetch your data you need to reload the table (updated code above). You can use a refresh control (declare it as a class property).
var refreshControl: UIRefreshControl!
Then when you finish getting your data you can refresh:
self.MyTableView.reloadData()
self.refreshControl?.endRefreshing()
This will call your delegate methods to populate the rows and sections.

Apple Watch-2 page-based navigation display HTTP data on page changed

I try to create an page-based navigation watch app with Swift 2.
My app is below:
For both interfaces I have unique controllers named InterfaceController and EconomyInterfaceController.
In each controller I read some JSON data with function in controller init() function
func setFeed(url: String) {
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
let request = NSURLRequest(URL: NSURL(string: url)!)
let task: NSURLSessionDataTask = session.dataTaskWithRequest(request) { (data, response, error) -> Void in
if let data = data {
do {
let _data: AnyObject? = try NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments)
if let items = _data!["data"] as? NSArray{
self.tableOutlet.setNumberOfRows(items.count, withRowType: "row")
for (index, item) in items.enumerate() {
if let title = self.JSONString(item["title"]) {
if let spot = self.JSONString((item["spot"])) {
let news = newsItem(title: title, spot: spot)
let row = self.tableOutlet!.rowControllerAtIndex(index) as? RowController
row!.rowLabel!.setText(news.title) }
}
}
}
} catch {
}
}
}
task.resume()
}
As I know this is not best way for HTTP request because of all request run on main thread and on app startup. This is not my main question but if you have any offer I cannot refuse :)
My main question is if I call setFeed() method in willActivate() method and set table labels with
row!.rowLabel!.setText(news.title)
my app works but this is not a good choice because on each time page changed this updates content again and again.
If I call setFeed() method in init() method and set table labels with
row!.rowLabel!.setText(news.title)
Only app's first page being displayed and other pages not being displayed. What is wrong here?

Difficulty Returning A Dictionary From NSURL Session

I'm hoping someone an help me figure out a problem that has me scratching my brain! When I attempt this function using a NSData(contentsOfUrl... structure, this all works fine. However, I am attempting to use a NSURLSession for use on an Apple Watch app, and keep hitting an error;
...
class func fetchData() -> [Complication] {
var task: NSURLSessionDataTask?
let myURL = "http://www.myurl.com/sample.json"
let dataURL = NSURL(string: myURL)
let conf = NSURLSessionConfiguration.defaultSessionConfiguration()
conf.requestCachePolicy = NSURLRequestCachePolicy.ReloadIgnoringCacheData
let session = NSURLSession(configuration: conf)
task = session.dataTaskWithURL(dataURL!) { (data, res, error) -> Void in
if let e = error {
print("dataTaskWithURL fail: \(e.debugDescription)")
return
}
var dataSet = [Complication]()
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as! NSArray
for item in json {
let name: String? = item["name"] as? String
let percent: Int? = item["percent"] as? Int
let timeFromNow: Int? = item["timeFromNow"] as? Int
let myData = Complication(
name: name!,
percent: percent!,
timeFromNow: timeFromNow!
)
dataSet.append(myData)
}
} catch {
print(error)
}
}
return dataSet
//THIS LINE THROWS THE ERROR
}
...
When attempting to return my dataSet array, I receive the error Instance member 'dataSet' cannot be used on type 'Complication'. As mentioned, however, this does seem to work if I were to use a NSData(contentsOfUrl... instead of a NSURLSession, which is where I am stuck!
The data task is a closure that is executed asynchronously. Its return statements returns from the closure, not from the outer function.
Since the closure is executed asynchronously it makes no sense to return data from it: the return type is Void.
You should organize your code differently, e.g. using a completion handler.
Hint: search for "swift return closure" in SO. You will find plenty of questions similar to yours and a number of good answers and suggestions.

Resources