Reading and parsing a file from dropbox in swift - ios

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!

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.

Advice on extracting information from web data (Swift)

I want to find a way to extract multiple items from web data in Swift. I'm looking to extract driver IDs from the following XML data after calling the URL in swift:
view-source: http://ergast.com/api/f1/current/last/results (view source in chrome)
Currently I have a solution which only picks out the first value. Ideally I would like to pick all the values and arrange them in an array for later use.
var wasSuccessful = false
let attemptedUrl = NSURL(string: "http://ergast.com/api/f1/current/last/results")
if let url = attemptedUrl {
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
if let urlContent = data {
let webContent = NSString(data: urlContent, encoding: NSUTF8StringEncoding)
let websiteArray = webContent?.componentsSeparatedByString("<Driver driverId=\"")
if websiteArray!.count > 1 {
let driverArray = websiteArray![1].componentsSeparatedByString("\" code=")
if driverArray.count > 1 {
wasSuccessful = true
let driverSummary = driverArray[0]
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.lblDrivers.text = driverSummary // just to confirm drivers are showing up
})
}
}
}
if wasSuccessful == false {
self.lblDrivers.text = "Sorry, we could not find the results."
}
}
task.resume()
} else { //if url couldnt be created
self.lblDrivers.text = "Sorry, we could not find the results."
}
}
This library solves your problem well: Fuzi
It support XPath and CSS queries. In your case, define a XML namespace "http://ergast.com/mrd/1.4" with prefix "ns" then use a XPath query "//ns:Driver" returns all elements named Driver as a Sequence.
The reason we need to define a namespace is that the XML you provide has its root element in namespace "http://ergast.com/mrd/1.4"
If you don't know XPath well, refer to this: https://en.wikipedia.org/wiki/XPath
import Fuzi
let attemptedUrl = NSURL(string: "http://ergast.com/api/f1/current/last/results")
if let url = attemptedUrl {
let task = NSURLSession.sharedSession().dataTaskWithURL(url) { (data, response, error) -> Void in
do {
guard let data = data else {
print("failed to get data")
return
}
let doc = try XMLDocument(data: data)
doc.definePrefix("ns", defaultNamespace: "http://ergast.com/mrd/1.4")
let drivers = doc.xpath("//ns:Driver")
for driver in drivers {
print(driver["driverId"])
print(driver["code"])
print(driver["url"])
}
if drivers.isEmpty {
print("No drivers found")
}
} catch let error {
print(error)
}
}
task.resume()
} else { //if url couldnt be created
print("Sorry, we could not find the results.")
}
You can use NSXMLParser for that. Here is a tutorial on how to use that class to parse an XML file.
However there is a number of libraries out there that make XML parsing a bit easier if you have never worked with NSXMLParser.
Here is a small list of XMLParser libraries that you could try out:
https://github.com/tadija/AEXML
https://github.com/drmohundro/SWXMLHash

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.

Display array after action

I have a little app that downloads some names from the web, and then appends them to an array.
func fetchTitle(identifier: String, completion: (title: String) -> Void) {
let profileUrl = NSURL(string:"http://www.facebook.com/" + identifier)!
let task = NSURLSession.sharedSession().dataTaskWithURL(profileUrl) {
(data, response, error) -> Void in
if let urlContent = data {
let webContent = NSString(data: urlContent, encoding: NSUTF8StringEncoding)
let websiteArray = webContent!.componentsSeparatedByString("pageTitle\">")
let secondArray = websiteArray[1].componentsSeparatedByString("</title>")
let title = secondArray[0]
completion(title: title)
print(title)
}
}
task.resume()
}
//print(newArray)
var titles = [String]()
//let identifiers = ["100001986741004","100003866283798","100003455181526"]
let queue = dispatch_queue_create("titles", DISPATCH_QUEUE_SERIAL)
dispatch_apply(newArray.count, queue) { index in
let identifier = newArray[index]
fetchTitle(identifier) { title in
dispatch_async(queue) {
titles.append(title)
array.append(title)
}
}
}
I know it's pretty complicated, because it takes numbers from an array and turns them into names downloaded from the web, but never mind about that. The problem is, when I print title, it gives me the names, so I assume it does append them to the array, but when I print the array, it gives me no result.. I think this is because it takes a little while to download the data from the web, and the print happens immediately, but how to I delay the print (or display into table view) until the download is complete?
Any help is appreciated!
Thank you very much!
I don't know about swift, but when your data has been retrieved, in Objective-C you can call [tableView reloadData]; should be simple enough to translate to Swift

Resources