Swift 5 : Escaping closure captures 'inout' parameter - ios

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.

Related

Reading and parsing a file from dropbox in swift

I am following a tutorial on how to read and parse a csv file from dropbox in swift. However, the tutorial is 4 years old and my code is not compiling in swift5. The code example is copied below and the link to the original video tutorial is here https://www.youtube.com/watch?v=O6AKHAXpji0
I am getting two errors.
Error 1:
on the let request = line of callFileFromWeb(){}
'NSURL' is not implicitly convertible to 'URL'; did you mean to use 'as' to explicitly convert?
Error 2:
and on let session = ... within the httpGet(){}
'NSURLSession' has been renamed to 'URLSession'
When I try to implement the proposed fix for error two then I get another error
Cannot call value of non-function type 'URLSession`
Any ideas what should I be adjusting for it to work in swift5?
var items:[(days:String, city:String, inches: String)]?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
callFileFromWeb()
}
func callFileFromWeb(){
let request = NSMutableURLRequest(URL: NSURL(string: "https://dl.dropboxusercontent.com/u/2813968/raindata.txt")!)
httpGet(request){
(data, error) -> Void in
if error != nil {
print(error)
} else {
print(data)//PRINTING ALL DATA TO CONSOLE
let delimiter = ":"
self.items = []
let lines:[String] = data.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet()) as [String]
for line in lines {
var values:[String] = []
if line != "" {
values = line.componentsSeparatedByString(delimiter)
// Put the values into the tuple and add it to the items array
print(values[2])//PRINTING LAST COLUMN
let item = (days: values[0], city: values[1], inches: values[2])
self.items?.append(item)
}}//all good above
// self.AddDataToDatabase()
}//there was an error
}//end of request
}//end of get data from web and load in database
func httpGet(request: NSURLRequest!, callback: (String, String?) -> Void) {
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request){
(data, response, error) -> Void in
if error != nil {
callback("", error!.localizedDescription)
} else {
let result = NSString(data: data!, encoding:
NSASCIIStringEncoding)!
callback(result as String, nil)
}
}
task.resume()
}
The final goal is to be able to read a file from drop box. The file updates weakly, so when users launch the app they always have access to the most updated version of the file, rather than having to re download the app when the file updates. Is this the correct approach to do this?
A framework that proved useful in past projects with parsing CSV files is:
CSwiftV -> https://github.com/Daniel1of1/CSwiftV
Updated: 9/23/2020
Let me demonstrate it by refactoring your callFileFromWeb():
func callFileFromWeb() {
let dropboxURL = URL(string: "https://dl.dropboxusercontent.com/u/2813968/raindata.txt")
URLSession.shared.dataTask(with: dropboxURL!) { data, response, error in
guard let urlData = data, error == nil else {
return
}
let unparsedCSV = String(data: urlData, encoding: String.Encoding.utf8) ?? "Year,Make,Model,Description,Price\r\n1997,Ford,E350,descrition,3000.00\r\n1999,Chevy,Venture,another description,4900.00\r\n"
let csv = CSwiftV(with: unparsedCSV)
let rows = csv.rows
var iteration = 0
for row in rows {
// Assuming you want the last row
if iteration == rows.count - 1 {
let item = (days: row[0], city: row[1], inches: row[2])
self.items?.append(item)
}
iteration += 1
}
}
}
One other thing, remember that you need to download CSSwiftV from Github and copy the original CSwiftV.swift file into your project.
Hope this helps!

Passing Json object based on user selection in a tableview swift

ScreenshotHi all I'm working on a project where when the user selects a particular cell in a table view, it should show them the data( which is a JSON Decoded object), Now I did all the networking stuff but not sure how to pass the values in such a way that when the user selects a particular cell, corresponding values should appear.
This is the contents of my tableview
var items = [Canada,Delhi,Mumbai,London]
When the user selects pen then the first value(kindly see my screenshot) should be displayed, Also I don't want to store these values as a hardcoded value, I want it to update whenever the user taps on that cell, I've added my networking struct, I'm new here so forgive me if there is any error in my question.
Thank You!
struct StateManager {
func geturl(){
let url = "https://api.covidindiatracker.com/state_data.json"
networking(stateUrl: url)
}
func networking(stateUrl : String){
if let url = URL(string: stateUrl){
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error != nil{
print("Error fetching Data")
return
}else{
//This is the decoded Data
if let safeData = data{
do{
let json = try JSON(data: safeData)
for i in 0..<38{
if let total = json[i]["confirmed"].int{
print(total)
}
}
}catch{
print("Error ")
}
}
}
}
task.resume()
}
}
}

'[ResponseData]' to expected argument type 'ResponseData'

I'm creating an application which are fetching data from an API. I've created the "API call" in a separate class so I can use the same call multiple times. But it does not return the value as I expect it to.
In ViewController A
let data = JsonData.init()
data.downloadJsonData(urlString: urlString) { (responseArray) in
dataArray.append(responseArray)
print(self.dataArray)
}
I'm getting the error at dataArray.append(responseArray):
Cannot convert value of type '[ResponseData]' to expected argument type 'ResponseData'
In JsonData class
class JsonData{
var dataArray:[ResponseData] = []
func downloadJsonData(urlString: String, completed: #escaping (Array<ResponseData>) -> ()){
guard let url = URL(string: urlString) else {return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else{
return
}
do{
self.dataArray = [try JSONDecoder().decode(ResponseData.self, from: data)]
//Complete task in background
DispatchQueue.main.async {
completed(self.dataArray)
}
}
catch let jsonErr{
print(jsonErr)
}
}.resume()
}
}
I assume the problem is at:
DispatchQueue.main.async{
completed(self.dataArray)
}
So I would like to return the array back to the correct class once it fetched the data from the API. What could I have done wrong? Any help would be much appreciated.
The error is clear: You are using the (wrong) API for appending a single element
Replace
dataArray.append(responseArray)
with
self.dataArray.append(contentsOf: responseArray)
Side note:
Setting and later appending the items again makes no sense. Use a local variable.
Replace
self.dataArray = [try JSONDecoder().decode(ResponseData.self, from: data)]
//Complete task in background
DispatchQueue.main.async {
completed(self.dataArray)
}
with (a different name makes it clearer)
let result = try JSONDecoder().decode(ResponseData.self, from: data)
//Complete task in background
DispatchQueue.main.async {
completed([result])
}

Swift || Returning a class that can be used by other methods from an API call

I am making a call to HERE Weather API from a mobile App and need to return the current weather as an object so that other methods can use that information for a given time period (eg. 30 min update intervals).
I have a rough understanding of asynchronous calls based on this site https://fluffy.es/return-value-from-a-closure/ but I am still running into my problem where I can't access the Weather object I create outside of the closure of the completionHandler.
I have provided the Get method from my Weather class. I have also provided a call to that Get method in the AppDelegates file.
public static func Get(lat:String, long:String, completionHandler: #escaping (Weather?) -> Void) {
//Keys go here
var request = URLRequest(
url: URL(string: "https://weather.cit.api.here.com/weather/1.0/report.json?app_code=-fJW5To2WdHdwQyYr_9ExQ&app_id=2m83HBDDcwAqTC2TqdLR&product=observation&latitude="+lat+"&longitude="+long+"&oneobservation=true")!)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request, completionHandler: { dat, response, error in
do {
guard let json = try? JSONSerialization.jsonObject(with: dat as! Data, options: []) else {throw JSONParseError.missingJSON}
//parse json for dictionaries
guard let response = json as? [String: Any] else {throw JSONParseError.responseNotADictionary}
guard let observations = response["observations"] as? [String: Any] else {throw JSONParseError.observationsNotADictionary}
guard let locationJSON = observations["location"] as? [Any] else {throw JSONParseError.locationNotAnArray}
guard let location = locationJSON.first as? [String: Any] else {throw JSONParseError.locationNotADictionary}
guard let observationJSON = location["observation"] as? [Any] else {throw JSONParseError.observationNotAnArray}
guard let observation = observationJSON.first as? [String: Any] else {throw JSONParseError.observationNotADictionary}
//search dictionaries for values
guard let feedCreation = response["feedCreation"] as? String else {throw JSONParseError.missingFeedCreationObject}
guard let city = location["city"] as? String else {throw JSONParseError.missingCityObject}
guard let state = location["state"] as? String else {throw JSONParseError.missingStateObject}
guard let temperature = observation["temperature"] as? String else {throw JSONParseError.missingTemperatureObject}
guard let icon = observation["icon"] as? String else {throw JSONParseError.missingIconObject}
guard let iconName = observation["iconName"] as? String else {throw JSONParseError.missingIconNameObject}
//create weather object and return
guard let currentWeather = Weather(feedCreation: feedCreation,state: state,city: city,temperature: temperature,icon: icon,iconName: iconName) else {throw WeatherObjectCreationError.objectCreationFailed}
completionHandler(currentWeather)
} catch {
print("JSON Serialization error")
}
}).resume()
}
Weather.Get(lat:"32.9005", long:"-96.7869", completionHandler: {currentWeather in
if let testWeather = currentWeather{
//Works fine
print(testWeather.city)
print("completionHandler")
}
})
//Error says testWeather is not intialized
print(testWeather.city)
At the end of my Weather.Get call I should be able to access the testWeather object from other methods. Specifically, a method that modifies the speed limit based on the current weather in the area, which is returned by the Weather.Get call.
What you have not understood is that the completion handler in your GET call is every bit as asynchronous as the original Weather API call. Thus you cannot run any code after it that depends on what's in it. The order of events is as follows:
// 1
Weather.Get(lat:"32.9005", long:"-96.7869", completionHandler: {currentWeather in
if let testWeather = currentWeather{ // 3!!!
print(testWeather.city) // 4!!!!!!
print("completionHandler")
}
})
print(testWeather.city) // 2
Moreover, what is the testWeather in the last line? Is it a property, self.testWeather? It would need to be. But even if it is, you have neglected to give self.testWeather a value; the if let testWeather is a different testWeather (it is local to the completion handler only and cannot "leak" out of it). However, even if you had done that, it still wouldn't work, because the code would still run in the wrong order:
// 1
Weather.Get(lat:"32.9005", long:"-96.7869", completionHandler: {currentWeather in
if let testWeather = currentWeather{ // 3
print(testWeather.city) // 4
print("completionHandler")
self.testWeather = testWeather // 5; better but it won't help the _next_ print
}
})
print(testWeather.city) // 2
Still, remembering to write to self.testWeather (5) would at least allow other code to access this value, provided it runs later.

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.

Resources