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
Related
Currently, I can fetch the download url by file name via firebase storage reference.
I would like to retrieve all download URLS in a specific child without using a file name and only using the last child name.
Simply adding every download url in a list/array
How can I accomplish this with my given reference.
func getDownloadURL() {
let ref = Storage.storage().reference()
let fileName = "Lessons_Lesson1_Class1.mp3"
let starsRef = ref.child("Daily Meditations").child("Lessons").child("Lesson 1").child(fileName)
// Fetch the download URL
starsRef.downloadURL { url, error in
if let error = error {
// Handle any errors
print(error)
} else {
// Get the download URL for 'Lessons_Lesson1_Class1.mp3'
print(url)
}
}
}
Firebase Refrence Docs
let stg = Storage.storage().reference()
let path = "Daily Meditations/Lessons/Lesson 1"
stg.child(path).listAll { (list, error) in
if let error = error {
print(error)
} else {
let inStorage = list.items.map({ $0.name })
print(inStorage) // an array of file names in string format
}
}
I assume spaces are allowed in path names since you're using them. To list all of the files in a path, use listAll. The method will return a StorageListResult object which I've named list.
https://firebase.google.com/docs/reference/swift/firebasestorage/api/reference/Classes/StorageListResult
So I was able to combine List all files and Download URL to achieve what I was trying to accomplish from the firebase documentation.
Here is the code:
func getDownloadURl() {
let ref = Storage.storage().reference()
let storageReference = ref.child("Lessons/Lesson 1")
storageReference.listAll { (result, error) in
if let error = error {
print(error)
}
for item in result.items {
//List storage reference
let storageLocation = String(describing: item)
let gsReference = Storage.storage().reference(forURL: storageLocation)
// Fetch the download URL
gsReference.downloadURL { url, error in
if let error = error {
// Handle any errors
print(error)
} else {
// Get the download URL for each item storage location
print(url!)
}
}
}
}
}
If anyone is using with VUE 2.6 and TS, here is my workaround
Imports
import {
getStorage,
ref,
getDownloadURL,
listAll,
StorageReference,
} from "firebase/storage";
async mounted(): Promise<void> {
const storage = getStorage();
const imageRefs = await listAll(ref(storage, "SOME BUCKET"))
.then((refs) => {
return refs.items;
})
.catch((error) => {
// Handle any errors
});
(imageRefs as StorageReference[]).forEach((item) => {
console.log(item);
getDownloadURL(item).then((downloadURL) => {
console.log(downloadURL);
this.model.listFiles.push(downloadURL);
});
});
console.log(imageRefs);
console.log(this.model.listFiles);
},
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!
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])
}
I'm new to RxSwift. I have a BackendProvider with handles the communication with my API. I want to have a config file sync so that I can retrieve some parameters dynamically. I have a fallback case with a local stored JSON file that I can to access in case my API is not reachable or my JSON parsing fails:
ConfigFileBackendService
open func getLatestConfig() -> Observable<ConfigFile?> {
let urlString = IoC.urlProviderService.getConfigFileUrl()?.absoluteString ?? ""
let configFileJSONData = IoC.backendCommunicationService.getJsonData(url: urlString)
return configFileJSONData.map { data in
if let configFile = try? JSONDecoder().decode(ConfigFile.self, from: data) {
return configFile
} else {
return nil
}
}
}
ConfigFileProcessService
This is the one that falls back to the local stored file:
func getConfigFile() -> Observable<ConfigFile> {
return IoC.configFileBackendService.getLatestConfig()
.map { configFile in
guard let configFile = configFile else { fatalError() }
return configFile
}
.catchError { error in
// Use default config
let localURL = IoC.urlProviderService.getLocalConfigFileUrl()
do {
let data = try Data(contentsOf: localURL)
let configFile = try JSONDecoder().decode(ConfigFile.self, from: data)
return Observable.just(configFile)
} catch {
fatalError("Error loading local config")
}
}
}
This approach works, but I'm having doubts with the .map / .catchError blocks. Is there a better way to handle the error case? Maybe should I go with, onNext and then onError? Thanks in advance!
What you have looks good except for the multiple approaches to handling errors. In one case you use a try? and another uses do... catch and presumably your getJsonData(url:) can emit an Observable error. You are all over the place. I suggest you pick one error handling system and stick to it. The most flexible one is Event.error. So something like this:
func getLatestConfig() -> Observable<ConfigFile> {
let urlString = IoC.urlProviderService.getConfigFileUrl()?.absoluteString ?? ""
let configFileJSONData = IoC.backendCommunicationService.getJsonData(url: urlString)
return configFileJSONData.map { try JSONDecoder().decode(ConfigFile.self, from: $0) }
}
Note that I'm just letting decoding errors route into an Observable error event. No need to deal with nil that way.
func getConfigFile() -> Observable<ConfigFile> {
return IoC.configFileBackendService.getLatestConfig()
.catchError { _ in
let localURL = IoC.urlProviderService.getLocalConfigFileUrl()
let data = try! Data(contentsOf: localURL)
let configFile = try! JSONDecoder().decode(ConfigFile.self, from: data)
return Observable.just(configFile)
}
}
Since you are crashing if either try fails anyway, just put a ! on them. It has the same effect. You should consider putting the error block into a separate, testable, function because there's no guarantee you are going to hit it during a regular run of the program and it could be broken without you ever realizing it.
Lastly, with the above there is no reason to provide an onError: handler in a subscribe because the getConfigFile() observable will never emit an error. You might want to have the function return a Driver instead to make the fact more explicit.
Following #Daniel T. approach, I got rid of nil and made the code crash only if the local config file loading process fails:
open class ConfigFileBackendService: ConfigFileBackendServiceProtocol {
open func getLatestConfig() -> Observable<ConfigFile> {
let urlString = IoC.urlProviderService.getConfigFileUrl()
let configFileJSONData = IoC.backendCommunicationService.getJsonData(url: urlString)
return configFileJSONData.map { try JSONDecoder().decode(ConfigFile.self, from: $0) }
}
}
class ConfigFileProcessService: ConfigFileProcessServiceProtocol {
func getConfigFile() -> Observable<ConfigFile> {
return IoC.configFileBackendService.getLatestConfig()
.catchError { [weak self] error in
// Load local config file
NSLog(error.localizedDescription)
guard let strongSelf = self else { fatalError() }
return Observable.just(strongSelf.getLocalFile())
}
}
private func getLocalFile() -> ConfigFile {
let localURL = IoC.urlProviderService.getLocalConfigFileUrl()
guard
let data = try? Data(contentsOf: localURL),
let configFile = try? JSONDecoder().decode(ConfigFile.self, from: data)
else { fatalError("Error loading local config") }
return configFile
}
}
I am a fairly decent Objective C developer, and I am now learning Swift (of which I am finding quite difficult, not only because of new concepts, such as optionals, but also because Swift is continually evolving, and much of the available tutorials are severely outdated).
Currently I am trying parse a JSON from a url into an NSDictionary and then use one of its value to display an image (which is also a url). Something like this:
URL -> NSDictionary -> init UIImage from url -> display UIImage in UIImageView
This is quite easy in Objective C (and there may even be a shorter answer):
NSURL *url = [NSURL URLWithString:#"https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY"];
NSData *apodData = [NSData dataWithContentsOfURL:url];
NSDictionary *apodDict = [NSJSONSerialization JSONObjectWithData:apodData options:0 error:nil];
The above code snippet gives me back a standard NSDictionary, in which I can refer to the "url" key to get the address of the image I want to display:
"url" : "https://apod.nasa.gov/apod/image/1811/hillpan_apollo15_4000.jpg"
This I then convert into a UIImage and give it to a UIImageView:
NSURL *imageURL = [NSURL URLWithString: [apodDict objectForKey:#"url"]];
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
UIImage *apodImage = [UIImage imageWithData:imageData];
UIImageView *apodView = [[UIImageView alloc] initWithImage: apodImage];
Now, I am basically trying to replicate the above Objective C code in Swift but continuously run into walls. I have tried several tutorials (one of which actually did the exact same thing: display a NASA image), as well as find a few stack overflow answers but none could help because they are either outdated or they do things differently than what I need.
So, I would like to ask the community to provide the Swift 4 code for the these problems:
1. Convert data from url into a Dictionary
2. Use key:value pair from dict to get url to display an image
If it is not too much already, I would also like to ask for detailed descriptions alongside the code because I would like the answer to be the one comprehensive "tutorial" for this task that I believe is currently not available anywhere.
Thank you!
First of all I'm pretty sure that in half a year you will find Objective-C very complicated and difficult. 😉
Second of all even your ObjC code is discouraged. Don't load data from a remote URL with synchronous Data(contentsOf method. Regardless of the language use an asynchronous way like (NS)URLSession.
And don't use Foundation collection types NSArray and NSDictionary in Swift. Basically don't use NS... classes at all if there is a native Swift counterpart.
In Swift 4 you can easily decode the JSON with the Decodable protocol directly into a (Swift) struct,
the URL string can be even decoded as URL.
Create a struct
struct Item: Decodable {
// let copyright, date, explanation: String
// let hdurl: String
// let mediaType, serviceVersion, title: String
let url: URL
}
Uncomment the lines if you need more than the URL.
And load the data with two data tasks.
let url = URL(string: "https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY")!
let task = URLSession.shared.dataTask(with: url) { (data, _, error) in
if let error = error { print(error); return }
do {
let decoder = JSONDecoder()
// this line is only needed if all JSON keys are decoded
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(Item.self, from: data!)
let imageTask = URLSession.shared.dataTask(with: result.url) { (imageData, _, imageError) in
if let imageError = imageError { print(imageError); return }
DispatchQueue.main.async {
let apodImage = UIImage(data: imageData!)
let apodView = UIImageView(image: apodImage)
// do something with the image view
}
}
imageTask.resume()
} catch { print(error) }
}
task.resume()
You can use this extension
extension UIImage {
public static func loadFrom(url: URL, completion: #escaping (_ image: UIImage?) -> ()) {
DispatchQueue.global().async {
if let data = try? Data(contentsOf: url) {
DispatchQueue.main.async {
completion(UIImage(data: data))
}
} else {
DispatchQueue.main.async {
completion(nil)
}
}
}
}
}
Using
guard let url = URL(string: "http://myImage.com/image.png") else { return }
UIImage.loadFrom(url: url) { image in
self.photo.image = image
}
Since image loading is a trivial and at the same time task which could be implemented in many different ways, I would recommend you to not "reinvent the wheel" and have a look to an image loading library such as Nuke, since it already covers most of the cases you might need during your development process.
It allows you to load and show image asynchronously into your view, using simple api:
Nuke.loadImage(with: url, into: imageView)
And also if you need - to specify how image should be loaded and presented:
let options = ImageLoadingOptions(
placeholder: UIImage(named: "placeholder"),
failureImage: UIImage(named: "failure_image"),
contentModes: .init(
success: .scaleAspectFill,
failure: .center,
placeholder: .center
)
)
Nuke.loadImage(with: url, options: options, into: imageView)
Create an UIIimageView Extension and the following code
extension UIImageView {
public func imageFromServerURL(urlString: String) {
self.image = nil
let urlStringNew = urlString.replacingOccurrences(of: " ", with: "%20")
URLSession.shared.dataTask(with: NSURL(string: urlStringNew)! as URL, completionHandler: { (data, response, error) -> Void in
if error != nil {
print(error as Any)
return
}
DispatchQueue.main.async(execute: { () -> Void in
let image = UIImage(data: data!)
self.image = image
})
}).resume()
}}
and
self.UploadedImageView.imageFromServerURL(urlString: imageURLStirng!)
I have just extended on vadian's answer, separated some concerns to clearly understand the basics. His answer should suffice.
First, you have to build your structure. This will represent the JSON structure you retrieved from the webservice.
struct Item: Codable {
let url, hdurl : URL,
let copyright, explanation, media_type, service_version, title : String
}
Then make you request methods. I usually create a separate file for it. Now, vadian mentioned about completion handlers. These are represented by escaping closures. Here, closure ()-> is passed on both functions and called having the decoded data as argument.
struct RequestCtrl {
func fetchItem(completion: #escaping (Item?)->Void) {
let url = URL(string: "https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY")!
//URLSessionDataTask handles the req and returns the data which you will decode based on the Item structure we defined above.
let task = URLSession.shared.dataTask(with: url) { (data, _, _) in
let jsonDecoder = JSONDecoder()
if let data = data,
let item = try? jsonDecoder.decode(Item.self, from: data){
//jsonDecoder requires a type of our structure represented by .self and the data from the request.
completion(item)
} else {
completion(nil)
}
}
task.resume()
}
func fetchItemPhoto(usingURL url: URL, completion: #escaping (Data?)-> Void) {
let task = URLSession.shared.dataTask(with: url) { (data, _, _) in
if let data = data { completion(data) } else { completion(nil) }
}
task.resume()
}
}
Now in you ViewController, call your request and handle the execution of your closure.
class ViewController: UIViewController {
let requestCtrl = RequestCtrl()
override func viewDidLoad() {
super.viewDidLoad()
requestCtrl.fetchItem { (fetchedItem) in
guard let fetchedItem = fetchedItem else { return }
self.getPhoto(with: fetchedItem)
}
}
func getPhoto(with item: Item) {
requestCtrl.fetchItemPhoto(usingURL: item.url) { (fetchedPhoto) in
guard let fetchedPhoto = fetchedPhoto else { return }
let photo = UIImage(data: fetchedPhoto)
//now you have a photo at your disposal
}
}
}
These are not the best of practices since I am also still learning, so by all means do some research on topics especially closures, ios concurrency and URLComponents on Apple's documentation :)
you need to convert url into string and data to add in imageview
let imageURL:URL=URL(string: YourImageURL)!
let data=NSData(contentsOf: imageURL)
Yourimage.image=UIImage(data: data! as Data)
First add the pod in Podfile
pod 'Alamofire',
pod 'AlamofireImage'
you can check this link for install pods => https://cocoapods.org/pods/AlamofireImage
// Use this function for load image from URL in imageview
imageView.af_setImage(
withURL: url,
placeholderImage: placeholderImage //its optional if you want to add placeholder
)
Check this link for method of alamofireImage
https://github.com/Alamofire/AlamofireImage/blob/master/Documentation/AlamofireImage%203.0%20Migration%20Guide.md
Update for Xcode 13.3 , Swift 5
To load the Image asynchronously from a URL string, use this extension:
extension UIImageView {
public func getImageFromURLString(imageURLString: String) {
guard let imageURL = URL(string: imageURLString) else { return}
Task {
await requestImageFromURL(imageURL)
}
}
private func requestImageFromURL(_ imageURL: URL) async{
let urlRequest = URLRequest(url: imageURL)
do {
let (data, response) = try await URLSession.shared.data(for: urlRequest)
if let httpResponse = response as? HTTPURLResponse{
if httpResponse.statusCode == 200{
print("Fetched image successfully")
}
}
// Loading the image here
self.image = UIImage(data: data)
} catch let error {
print(error)
}
}
}
Usage:
imageView.getImageFromURLString(imageURLString: "https://apod.nasa.gov/apod/image/1811/hillpan_apollo15_4000.jpg")