Parsing a JSON and handling error response from API in RxSwift - ios

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

Related

'[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])
}

How to load a JSON tableView and save data even on dismiss?

I currently have a button that opens a TableViewController and loads the data using JSON like the following:
private func JSON() {
print(facility)
guard let url = URL(string: "https://example/example/example"),
let sample = value1.addingPercentEncoding(withAllowedCharacters: .urlQueryValueAllowed)
else { return }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = "example1=\(example)".data(using: .utf8)
URLSession.shared.dataTask(with: request) { data, _, error in
guard let data = data else { return }
do {
self.JStruct = try JSONDecoder().decode([exampleStruct].self,from:data)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
catch {
print(error)
}
}.resume()
}
Then after I am done looking at the tableview I close it by doing:
self.dismiss(animated: true, completion: nil)
using a BarButtonItem.
The issue is every time the UIView opens it takes some time to load the data. Is there anyway to have the tableView load just once and when dismissed and re-opened just have the same data show that was already loaded before?
The best thing you can do is to store the data locally. Either use a local database or a plain text file to store the data. When you open the page check whether data is already present. If it is already present load it, and call the API in background silently to update the existing data. If data is not saved, call the API, load the data and save it locally.
func getFileURL() -> URL {
let fileName = "CacheData"
let documentDirURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let fileURL = documentDirURL.appendingPathComponent(fileName).appendingPathExtension("json")
return fileURL
}
func createFile(data: Data) {
let fileURL = getFileURL()
do {
try data.write(to: fileURL)
} catch let e {
print(e.localizedDescription)
}
}
func loadData() -> Data? {
let fileURL = getFileURL()
do {
let data = try Data(contentsOf: fileURL)
return data
} catch let e {
print(e.localizedDescription)
}
return nil
}
In your viewDidLoad method do something like:
let fileURL = getFileURL()
if FileManager.default.fileExists(atPath: fileURL.path) {
if let data = loadData() {
do {
self.JStruct = try
JSONDecoder().decode([exampleStruct].self,from:data)
DispatchQueue.main.async {
self.tableView.reloadData()
} catch {
print(error)
}
}
}
JSON()
And call the createFile when you get data from the API. You may need to write the file and load the file using a background queue to avoid overloading and freezing of your main thread.

How to download A LOT of files from S3 using the transfer utility?

I have several thousand images I want to download from a S3 bucket to an iOS App.
But I'm getting memory issues I'm unable to track down.
Here is my sketchy code:
let client = HttpClient<[SomeImage]>()
client.get(fromURL: URL(string: endpoint)!) {
(result, error) in
if let error = error {
self.log(message: "\(error)", level: .error)
return
}
if let result = result {
let downloadGroup = DispatchGroup()
var count = 0
// just assembling a list of s3 keys to download here...
for item in result {
for image in (item.images ?? []) {
let prefix = "\(image.key)/"
for key in ["\(globalGetThumbnailS3Key(byImageKey: image.key))",
"\(globalGetPreviewS3Key(byImageKey: image.key))"] {
count = count + 1
let completionHandler: AWSS3TransferUtilityDownloadCompletionHandlerBlock = {
(task, URL, data, error) in
if let error = error {
self.log(message: "\(error)", level: .error)
return
}
if let data = data, let localDir = FileManager.default.applicationSupportURL {
do {
let imageURL = localDir.appendingPathComponent(key)
FileManager.default.directoryExistsOrCreate(localDir.appendingPathComponent(prefix))
try data.write(to: imageURL)
self.log(message: "downloaded \(prefix)\(key) to \(imageURL.absoluteString)", level: .verbose)
} catch let error {
self.log(message: "\(error)", level: .error)
return
}
}
}
bgSyncQueue.async(group: downloadGroup) {
self.transferUtility.downloadData(fromBucket: "\(globalDerivedImagesBucket)", key: key,
expression: nil,
completionHandler: completionHandler).continueWith {
(task) in
if let error = task.error {
// iirc, this error is caused, if the task couldnt be created due to being offline
self.log(message: "\(error)", level: .error)
return nil
}
if let result = task.result {
// do something with the task?
return nil
}
return nil
}
}
}
}
}
self.log(message: "\(count) images to download...", level: .debug)
bgSyncQueue.activate()
downloadGroup.notify(queue: DispatchQueue.main) {
self.log(message: "All items downloaded?!")
}
}
}
}
So I put all calls to the transfer utility in a serial dispatch queue, which is initially inactive. Then I activate the queue and downloading starts just fine. But after a while the app crashes with "Message from debugger: Terminated due to memory issue."
The app is only consuming about 100M of memory though. What am I overlooking?
Rob's suggestion to use the "downloadToUrl" method was the way to go, without using GCD on my part. Thanks again, Rob!
The transferUtility seems to be a fine tool, though very badly documented.
Here is the simple code used to download about 20k of images:
for key in keys {
let imageURL = localDir.appendingPathComponent(key.1)
let completionHandler: AWSS3TransferUtilityDownloadCompletionHandlerBlock = {
(task, URL, data, error) in
if let error = error {
self.log(message: "failed downloading \(key.1): \(error)", level: .error)
DispatchQueue.main.async {
countingDown()
}
return
}
DispatchQueue.main.async {
countingDown()
if let onProgress = self.onProgress {
onProgress(100.0 - ((100.0 / Double(total)) * Double(count)))
}
}
//self.log(message: "downloaded \(key.1)")
}
transferUtility.download(to: imageURL, bucket: "\(globalDerivedImagesBucket)", key: key.1, expression: nil, completionHandler: completionHandler).continueWith {
(task) in
if let error = error {
self.log(message: "\(error)", level: .error)
DispatchQueue.main.async {
countingDown()
}
return nil
}
return nil
}
}
You may need to consider using an autoreleasepool to better manage the memory used by the bridged data types as detailed here
Exert from article (in case of link changes)
Consider the code:
func run() {
guard let file = Bundle.main.path(forResource: "bigImage", ofType: "png") else {
return
}
for i in 0..<1000000 {
let url = URL(fileURLWithPath: file)
let imageData = try! Data(contentsOf: url)
}
}
Even though we’re in Swift, this will result in the same absurd memory spike shown in the Obj-C example! This is because the Data init is a bridge to the original Obj-C [NSDatadataWithContentsOfURL] -- which unfortunately still calls autorelease somewhere inside of it. Just like in Obj-C, you can solve this with the Swift version of #autoreleasepool; autoreleasepool without the #:
autoreleasepool {
let url = URL(fileURLWithPath: file)
let imageData = try! Data(contentsOf: url)
}
Disclaimer: I am no expert in Swift or Objective-C advanced memory management but I have used this in a similar scenario with good results.

Modify Global Variable Inside Closure (Swift 4)

I am trying to modify the global variable currentWeather (of type CurrentWeather) using this function, which is meant to update said variable with the information retrieved from the URL and return a bool signifying its success. However, the function is returning false, as currentWeather is still nil. I recognize that the dataTask is asynchronous, and that the task is running in the background parallel to the application, but I don't understand what this means for what I'm trying to accomplish. I also am unable to update currentWeather after the do block, as weather is no longer recognized after exiting the block. I did try using "self.currentWeather", but was told it was an unresolved identifier (perhaps because the function is also global, and there is no "self"?).
The URL is not currently valid because I took out my API key, but it is working as expected otherwise, and my CurrentWeather struct is Decodable. Printing currentWeatherUnwrapped is also consistently successful.
I did look around Stack Overflow and through Apple's official documentation and was unable to find something that answered my question, but perhaps I wasn't thorough enough. I'm sorry if this is a duplicate question. Direction to any further relevant reading is also appreciated! I apologize for the lack of conformity to best coding practices - I'm not very experienced at this point. Thank you all so much!
func getCurrentWeather () -> Bool {
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else { return }
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
print(currentWeatherUnwrapped)
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
return currentWeather != nil
}
When you do an asynchronous call like this, your function will return long before your dataTask will have any value to return. What you need to do is use a completion handler in your function. You can pass it in as a parameter like this:
func getCurrentWeather(completion: #escaping(CurrentWeather?, Error?) -> Void) {
//Data task and such here
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else {
completion(nil, err)
return
}
//You don't need a do try catch if you use try?
let weather = try? JSONDecoder().decode(CurrentWeather.self, from: data)
completion(weather, err)
}.resume()
}
Then calling that function looks like this:
getCurrentWeather(completion: { (weather, error) in
guard error == nil, let weather = weather else {
if weather == nil { print("No Weather") }
if error != nil { print(error!.localizedDescription) }
return
}
//Do something with your weather result
print(weather)
})
All you need is a closure.
You cant have synchronous return statement to return the response of web service call which in itself is asynchronous in nature. You need closures for that.
You can modify your answer as below. Because you have not answered to my question in comment I have taken liberty to return the wether object rather than returning bool which does not make much sense.
func getCurrentWeather (completion : #escaping((CurrentWeather?) -> ()) ){
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/"
guard let url = URL(string: jsonUrlString) else { return false }
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else { return }
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
CurrentWeather.currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
completion(CurrentWeather.currentWeather)
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
completion(nil)
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
}
Assuming currentWeather is a static variable in your CurrentWeather class you can update your global variable as well as return the actual data to caller as shown above
EDIT:
As pointed out by Duncan in comments below, the above code executes the completion block in background thread. All the UI operations must be done only on main thread. Hence its very much essential to switch the thread before updating the UI.
Two ways :
1- Make sure you execute the completion block on main thread.
DispatchQueue.main.async {
completion(CurrentWeather.currentWeather)
}
This will make sure that whoever uses your getCurrentWeather in future need not worry about switching thread because your method takes care of it. Useful if your completion block contains only the code to update UI. Lengthier logic in completion block with this approach will burden the main thread.
2 - Else In completion block that you pass as a parameter to getCurrentWeather whenever you update UI elements make sure you wrap those statements in
DispatchQueue.main.async {
//your code to update UI
}
EDIT 2:
As pointed out by Leo Dabus in comments below, I should have run completion block rather than guard let url = URL(string: jsonUrlString) else { return false } That was a copy paste error. I copied the OP's question and in a hurry din realize that there is a return statement.
Though having a error as a parameter is optional in this case and completely depends on how you designed your error handling model, I appreciate the idea suggested by Leo Dabus which is more general approach and hence updating my answer to have error as a parameter.
Now there are cases where we may need to send our custom error as well for example if guard let data = data else { return } returns false rather than simply calling return you may need to return a error of your own which says invalid input or something like that.
Hence I have taken a liberty to declare a custom errors of my own and you can as well use the model to deal with your error handling
enum CustomError : Error {
case invalidServerResponse
case invalidURL
}
func getCurrentWeather (completion : #escaping((CurrentWeather?,Error?) -> ()) ){
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/"
guard let url = URL(string: jsonUrlString) else {
DispatchQueue.main.async {
completion(nil,CustomError.invalidURL)
}
return
}
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
if err != nil {
DispatchQueue.main.async {
completion(nil,err)
}
return
}
guard let data = data else {
DispatchQueue.main.async {
completion(nil,CustomError.invalidServerResponse)
}
return
}
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
CurrentWeather.currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
DispatchQueue.main.async {
completion(CurrentWeather.currentWeather,nil)
}
}
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
DispatchQueue.main.async {
completion(nil,jsonErr)
}
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
}
You fundamentally misunderstand how async functions work. You function returns before the URLSession's dataTask has even begun to execute. A network request may take multiple seconds to complete. You ask it to fetch some data for you, give it a block of code to execute ONCE THE DATA HAS DOWNLOADED, and then go on with your business.
You can be certain that the line after the dataTask's resume() call will run before the new data has loaded.
You need to put code that you want to run when the data is available inside the data task's completion block. (Your statement print(currentWeatherUnwrapped) will run once the data has been read successfully.)
As you pointed out, the data ask is async, meaning you do not know when it will be completed.
One option is to modify your wrapper function getCurrentWeather to be async as well by not providing a return value, but instead a callback/closure. Then you will have to deal with the async nature somewhere else though.
The other option which is what you probably want in your scenario is to make the data task synchronous like so:
func getCurrentWeather () -> Bool {
let jsonUrlString = "https://api.wunderground.com/api/KEY/conditions/q/\(state)/\(city).json"
guard let url = URL(string: jsonUrlString) else { return false }
let dispatchGroup = DispatchGroup() // <===
dispatchGroup.enter() // <===
URLSession.shared.dataTask(with: url) { (data, response, err) in
// check error/response
guard let data = data else {
dispatchGroup.leave() // <===
return
}
do {
let weather = try JSONDecoder().decode(CurrentWeather.self, from: data)
currentWeather = weather
if let currentWeatherUnwrapped = currentWeather {
print(currentWeatherUnwrapped)
}
dispatchGroup.leave() // <===
} catch let jsonErr {
print("Error serializing JSON: ", jsonErr)
dispatchGroup.leave() // <===
}
// cannot update currentWeather here, as weather is local to do block
}.resume()
dispatchGroup.wait() // <===
return currentWeather != nil
}
The wait function can take parameters, which can define a timeout. https://developer.apple.com/documentation/dispatch/dispatchgroup Otherwise your app could be stuck waiting forever. You will then be able to define some action to present that to the user.
Btw I made a fully functional weather app just for learning, so check it out here on GitHub https://github.com/erikmartens/NearbyWeather. Hope the code there can help you for your project. It's also available on the app store.
EDIT: Please understand that this answer is meant to show how to make async calls synchronous. I am not saying this is good practice for handling network calls. This is a hacky solution for when you absolutely must have a return value from a function even though it uses async calls inside.

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

Resources