Difficulty Returning A Dictionary From NSURL Session - return

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.

Related

JSON array inside of an array

I am looking to access a string that is located inside of a JSON array that is located inside of another array. I am accessing the JSON API using JSONDecoder. I am receiving errors when trying the various methods that I have used in the past when using JSON arrays.
Here is the code:
var country = [Results]()
struct Rating: Codable {
let results: [Results]
}
struct Results: Codable {
let iso_3166_1: String
let release_dates: [Release_Dates]
}
struct Release_Dates: Codable {
let certification: String
}
func loadRating() {
let id = filmId
let apiKey = ""
let url = URL(string: "https://api.themoviedb.org/3/movie/\(id)/release_dates?api_key=\(apiKey)")
let request = URLRequest(
url: url! as URL,
cachePolicy: URLRequest.CachePolicy.reloadIgnoringLocalCacheData,
timeoutInterval: 10 )
let session = URLSession (
configuration: URLSessionConfiguration.default,
delegate: nil,
delegateQueue: OperationQueue.main
)
let task = session.dataTask(with: request, completionHandler: { (dataOrNil, response, error) in
if let data = dataOrNil {
do { let rates = try! JSONDecoder().decode(Rating.self, from: data)
self.country = rates.results
let us = self.country.filter({ $0.iso_3166_1.contains("US") })
print(us)
}
}
})
task.resume()
}
us prints to console
[Film.DetailsView.Results(iso_3166_1: "US", release_dates: [Film.DetailsView.Release_Dates(certification: "PG-13")])]
I am trying to access the certification string.
What would be the correct method used to achieve this?
us is an array of Results.
To get the first certification use this:
print(us.first!.release_dates.first!. certification)
I am force unwrapping for brevity, you should properly do it with optional binding or the guard statement.
Pretty straightforward, the result of filter is an array and certification is in the array release_dates
let us = self.country.filter({ $0.iso_3166_1.contains("US") })
for item in us {
for releaseDate in item.release_dates {
print(releaseDate.certification)
}
}
Please name your struct member names lowerCamelCased by mapping the keys with CodingKeys or with the convertFromSnakeCase strategy.
If there is only one US item, use first
if let us = self.country.first({ $0.iso_3166_1.contains("US") }) {
for releaseDate in us.release_dates {
print(releaseDate.certification)
}
}

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

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

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.

Swift NSURL nil when running the application

When I run the application Xcode told me that
unexpectedly found nil while unwrapping an Optional value
at the url but the url isn't nil, can someone help?
here is the code
import Foundation
protocol WeatherUndergroundServiceByGeographicalDelegate{
func setWeatherByGeographical(weather:WeatherUnderground)
}
class WeatherUndergoundServiceByGeographical{
var delegate:WeatherUndergroundServiceByGeographicalDelegate?
func getWeatherFromWeatherUnderground(latitude:Double, longitude:Double){
let path = "http://api.wunderground.com/api/48675fd2f5485cff/conditions/geolookup/q/\(latitude,longitude).json"
let url = NSURL(string: path)
//session
let session = NSURLSession.sharedSession()
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Error is at here~~~~~~~~~~~~~~~~~~~~~~~~~
let task = session.dataTaskWithURL(url!) { (data:NSData?, response:NSURLResponse?, error:NSError?) -> Void in
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
let json = JSON(data: data!)
//parsing json weather condition from weather api. using swiftyJson
let name = json["current_observation"]["display_location"]["city"].string
let temp = json["current_observation"]["temp_c"].double
let windsp = json["current_observation"]["wind_mph"].double
//prasing the weather data
let weather = WeatherUnderground(cityName: name!, temperature: temp!, windSpeed: windsp!)
if self.delegate != nil{
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.delegate?.setWeatherByGeographical(weather)
})
}
}
task.resume()
}
}
You probably have error in your path string, try this:
let path = "http://api.wunderground.com/api/48675fd2f5485cff/conditions/geolookup/q/\(latitude),\(longitude).json"
The reason is that you are interpolating tuple value \(latitude,longitude) in the string, which adds extra space and makes url string invalid because space is not percent-escaped.
Instead you have to interpolate each value with a comma between them: \(latitude),\(longitude)
let path = "http://api.wunderground.com/api/48675fd2f5485cff/conditions/geolookup/q/\(latitude,longitude).json"
I think you mean:
let path = "http://api.wunderground.com/api/48675fd2f5485cff/conditions/geolookup/q/\(latitude),\(longitude).json"

How can I Init a Class with a Web Service Call in Swift?

I have a drug class which I would like to init by passing in a upc: Int. Inside the init I'd like to make a web service call, and populate the values in the class with the returned JSON (NSDictionary).
I have a start, but not a finish. I'm getting errors everywhere and can't seem to figure out how to best accomplish this. Maybe I'm going about this the whole wrong way. Could someone help?
Here's the code ..
init(upc: Int) {
let apiKey = "xx"
let baseURL = NSURL(string: "http://something.com/\(apiKey)/")
let getDrugByUpcURL = NSURL(string: "\(upc).json", relativeToURL: baseURL)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let usernamePasswordString = "user:pass"
let usernamePasswordData = usernamePasswordString.dataUsingEncoding(NSUTF8StringEncoding)
let base64EncodedCredential = usernamePasswordData!.base64EncodedStringWithOptions(nil)
let authString = "Basic \(base64EncodedCredential)"
config.HTTPAdditionalHeaders = ["Authorization": authString]
let session = NSURLSession(configuration: config)
let downloadTask: NSURLSessionDownloadTask = session.downloadTaskWithURL(getDrugByUpcURL!, completionHandler: { (location: NSURL!, response: NSURLResponse!, error: NSError!) -> Void in
if (error == nil) {
let dataObject = NSData(contentsOfURL: location)
println(dataObject)
let drugDictionary: NSDictionary = NSJSONSerialization.JSONObjectWithData(dataObject!, options: nil, error: nil) as NSDictionary
println(drugDictionary["din"])
drug_id = drugDictionary["drug_id"] as Int
din = drugDictionary["din"] as String
packsize = drugDictionary["packsize"] as Double
brand = drugDictionary["brand"] as String
//generic = drugDictionary["generic"] as String
strength = drugDictionary["strength"] as String
form = drugDictionary["form"] as String
upc = drugDictionary["upc"] as String
//priceGroup = drugDictionary["price_group"] as String
//manufacturer = drugDictionary["manufacturer"] as String
onHandQuantity = drugDictionary["onhand"] as Double
//vendorCost = drugDictionary["vendor_cost"] as Double
//basePrice = drugDictionary["base_price"] as Double
//discount = drugDictionary["discount"] as Double
//price = drugDictionary["price"] as Double
} else {
println(error)
}
})
downloadTask.resume()
}
An error I'm getting is on all property assignment lines: Cannot assign to 'drug_id' in 'self'.
The problem is that you are accessing those instance variables from a closure, the completion handler of downloadTaskWithURL:.
This is easily fixed by prefixing the variables with self.. So drug_id becomes self.drug_id.
Note however that what you are doing in the above code is probably a bad idea. Or at least a very uncommon design: I don't think it is a good idea to do asynchronous network requests in your class' initializer.
Since the call to NSURLSessionDownloadTask is asynchronous, your init() will return immediately with uninitialized data and then at some unspecified moment in time your class will be fully populated with the result from the web service call. You don't know what that moment in time is though, so you really have no good way of knowing when your instance is ready.
This is most likely not what you had in mind.

Resources