iOS Swift - URLSessionDataTask doesn't get updated JSON data on refresh - ios

I have a simple app where I get data from a JSON file stored in my own server in this way - I'm using SwiftyJSON:
func queryData(_ fileName:String) {
guard let url = URL(string: JSON_PATH + fileName + ".json") else {return} // JSON_PATH + fileName + ".json" is the complete path to my db.json file (see below)
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let dataResponse = data, error == nil else {
self.simpleAlert(error!.localizedDescription)
return
}
let jsonData = try! JSON(data: dataResponse)
print("ARRAY: \(jsonData)")
}
task.resume()
}
Here's my db.json file:
[
{
"objID":"GNoW3vszYz",
"string":"First (1) string",
"pointer":["pointer","yN76TF43i8"],
"boolean":true,
"number":123,
"fileURL":"https://example.com/uploads/01.jpg",
"array":["aaa","bbb","ccc"]
},
{
"objID":"yN76TF43lD",
"string":"Second (2) string",
"pointer":["pointer","GNoN3vsz2I"],
"boolean":false,
"number":12.55,
"fileURL":"https://example.com/uploads/02.jpg",
"array":["aaa","eee","fff"]
}
]
The problem is that if I manually edit my db.json file, let's say I change "number":123, into "number":5555, save and upload it again in my server and run my app again, the console log shows me the same JSON data as above, like if I had changed nothing.
I've tried to kill the app and run it again 2-3 times with Xcode, no success, the only way I can get the updated JSON file data is to delete the app and install it again via Xcode.
Is there a way to always get updated JSON data with URLSessionDataTask?
Thanks.

This is due to URLSession caching the data from the server, this is usually sent in the header of the json file coming from the server.
If you can't change anything on the server side then on you can just use the .ephemeral which apple documents here
use this code
let configuration = URLSessionConfiguration.ephemeral
let session = URLSession(configuration: configuration)
.ephemeral is used for data that will only be stored in ram and nothing will be stored on disk

I can think of two ways.
1) add current date time at the end of your url
2) Disable Local Cache.
For 1)
extension Date {
func currentTimeMillis() -> Int64 {
return Int64(self.timeIntervalSince1970 * 1000)
}
}
let url = URL(string: JSON_PATH + fileName + ".json?q=\(Date().currentTimeMillis())")
For 2)
let config = URLSessionConfiguration.default
config.requestCachePolicy = .reloadIgnoringLocalCacheData
config.urlCache = nil
let session = URLSession.init(configuration: config)
Taken From this SO

This behavior also can happen with text files (and other non-JSON files) stored on a web server and by using URLSession to fetch data from that text file. .ephemeral also works in this case.
if let url = URL(string: "http://www.somerandomsite.com/mydata.txt") {
let configuration = URLSessionConfiguration.ephemeral
var urlSession = URLSession(configuration: configuration).dataTask(with: url) {(data, response, error) in
if let error = error {
print(error)
}
if var textFile = String(data: data!, encoding: .utf8){
print(textFile)
}
}
urlSession.resume()

Related

How can I connect to a hard coded IP address using http in iOS 12+

I am trying to make a simple prototype with both a server (EC2 dev server) and a client. Where the server just has a hard IPv4 address (numbers) and send http results upon requests and the client is a prototype iOS 12+ app where I simply want to retrieve data from the server that is using http. How can I do so? Thanks in advance!
Read up on URLSession and how to fetch webside data into memory.
func startLoad() {
let url = URL(string: "http://IPADDRESS/")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
self.handleClientError(error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
self.handleServerError(response)
return
}
// Data is in the data-variable.
if let mimeType = httpResponse.mimeType, mimeType == "text/html",
let data = data,
let string = String(data: data, encoding: .utf8) {
// Avoid using the main-thread to return any data.
DispatchQueue.main.async {
// Return data to use in view/viewcontroller
self.webView.loadHTMLString(string, baseURL: url)
}
}
}
task.resume()
}
Other things you probably will want to get into is MVVM/MVC for managing data between views and Codables.

With iOS how to get JSON from website and save as it is?

I've been trying to figure out how to get the content of a JSON file, from a website, and then save that content - the actual { ... } object to a file in iOS (with XCode). Being new to iOS, I figured I would look online but nothing seems to be applicable. How do I actually get the JSON file content itself and not the byte size of the content?
Here is how the call is setup:
func test() {
let url = NSURL(string: "https://www.somesite.com/file.json")
let request = NSURLRequest(url: url! as URL)
let session = URLSession.shared
var json: Any
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error -> Void in
if let data = data {
**[PROBLEM APPEARS TO BE HERE]** json = try JSONSerialization.jsonObject(with: data, options[])
let dataToSave = try JSONSerialization.data(withJSONObject: json, options:[])
print(dataToSave)
}
})
task.resume()
}
I've also tried something like NSString(data: data, encoding: String.Encoding.utf8.rawValue) but that creates an escaped string of the content that, when saved to a file, is unrecognized by the native JSON decoder when it reads the file.
Though I've only dipped into iOS and XCode 11.5 for a couple of weeks now, I appear to have the answer to the question specified above. In short, the behavior of disk based reading and writing of JSON varies significantly from attempting to do the same thing with a asynchronous web request and requires attaching a "helper" function to complete saving website JSON content to the local disk.
BASE FUNCTION MAKING WEB REQUEST:
func test1() {
var targetUrl = NSURL(string: "https://www.somesite.com/file.json")
let request = NSURLRequest(url: targetUrl! as URL)
let session = URLSession.shared
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error -> Void in
if let data = data {
/* To avoid errors about "Invalid conversion from throwing function of type '(Data?,URLResponse?,Error?) throws -> Void' to non-throwing function type '(Data?,URLResponse?,Error?) -> Void'", when attempting to manipulate data here, pass data containing the file content to a helper function. */
self.test2Helper(contentData: data.self)
}
})
task.resume()
return
}
HELPER FUNCTION TO SAVE THE JSON CONTENT FROM THE WEBSITE:
func test2Helper(contentData: Data) {
/* Retrieve filepath information for app's Documents folder on device */
let appDocumentsFolder = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let fullPath = appDocumentsFolder.appendingPathComponent("website").appendingPathExtension("json")
/* For manually opening the file for verification, show the path */
print(fullPath)
/* Save the JSON content to the file */
do {
/* Build JSON Data Object */
let json = try JSONSerialization.jsonObject(with: contentData)
/* Save JSON Data to Documents */
let data = try JSONSerialization.data(withJSONObject: json, options: [])
try data.write(to: fullPath, options: [])
/* For testing, show completion */
print("JSON content from website saved to disk.")
}
catch {
print(error)
}
}

Run JSON Request in the background Swift 4

I need to run this code in the background if possible. Im getting a JSON Request that sometimes takes a while to load(lag is on the server side of the URL, not the code itself.).
I want to run the code below in the background if possible. Any ideas?
var stockData: Data!
var concatTickersString = ""
for object in dataArray.reversed() {
concatTickersString = concatTickersString + "," + object.symbol
}
let url = URL(string: "https://www.alphavantage.co/query?function=BATCH_STOCK_QUOTES&symbols=" + concatTickersString + "&apikey=IX58FUCXKD695JY0")
do {
stockData = try Data(contentsOf: url!)
let json = try JSON(data: stockData)
if let jsonArray = json["Stock Quotes"].array {
for ticker in jsonArray.reversed() {
if(jsonArray.count != 0){
let stockTicker = ticker["1. symbol"].string!
let stockPrice = ticker["2. price"].string!
self.watchListArray.append(WatchlistData(tickerName: stockTicker, tickerPrice: Double(stockPrice)?.currency))
}
}
tableView.isHidden = false
}
} catch {
print(error)
}
Its the server of the JSON that takes long I dont think its necessarily the Data(contents of)
I tried using dispatch_async but im getting no luck.
The lag is caused by the fact that Data(contentsOf:) is a synchronous method. As the documentation says,
Important
Don't use this synchronous method to request network-based URLs. For network-based URLs, this method can block the current thread for tens of seconds on a slow network, resulting in a poor user experience, and in iOS, may cause your app to be terminated.
Instead, for non-file URLs, consider using the dataTask(with:completionHandler:) method of the URLSession class. See Fetching Website Data into Memory for an example.
As you discovered through experimentation, placing this method in DispatchQueue.main.async does not make it asynchronous. Instead, follow the documentation's instruction.
This is the slightly modified example found at Fetching Website Data into Memory:
func startLoad() {
let url = URL(string: "https://www.example.com/")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
self.handleClientError(error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
self.handleServerError(response)
return
}
if let data = data,
let string = String(data: data, encoding: .utf8) {
DispatchQueue.main.async {
doSomething(with: string)
}
}
}
task.resume()
}

Getting JSON Response from API Swift

At an earlier point today, I was able to use this API and get a response in my iPhone app. The fact that I have been trying to debug this for so long is making be believe that I'm crazy! Attached is a screenshot of my console...
Here is my code pertaining to my API call. Using Apple's URLSession and following many stack overflow questions / Tutorials I can not get this thing to work.
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else {
print("request failed \(error)")
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: String], let result = json["result"] {
// Parse JSON
}
} catch let parseError {
print("parsing error: \(parseError)")
let responseString = String(data: data, encoding: .utf8)
print("raw response: \(responseString)")
}
}
task.resume()
Every time I get this interesting [BoringSSL] Error and the searching I've done regarding that has not produced effective in fixing whatever bug I have.
Like I said, earlier today I had this app working using the same API. I have tried the key that the website gave me and the test key they use on their site. Now that I think of it, I am going to use the exact URL from my code and the screenshot and take a screenshot from the response I get in my browser. See below:
Received above response with the exact URL being used in my app.
tried your API in my project. It worked. You can check the difference below:
let urlTest = URL(string: "https://www.zipcodeapi.com/rest/wvyR5aWjHNUF80Z6kmr1bTuNojfzhmvtcmfBD8QNo9qbNAHy9FvBISINKF3W5i9J/multi-distance.json/99501/99501,%2085001,%2072201/km")
var request = URLRequest(url: urlTest!)
request.httpMethod = "GET"
let session = URLSession(configuration: .default)
let task : URLSessionDataTask = session.dataTask(with: request) { (data, response, error) in
let statusCode = (response as! HTTPURLResponse).statusCode
if statusCode == 200{
do {
let json = try JSON(data:data!)
}
catch {
print("Could not convert JSON data into a dictionary.")
}
}
}
task.resume()
Printing description of json:
▿ {
"distances" : {
"85001" : 4093.922,
"72201" : 4962.6189999999997
}
}
May be you have to turn off Transport Layer Security, because that worked for me.
Go to your info.plist file and add a property named App Transport Secrity Settings and set its Allow Arbitrary loads option to NO
Hope this helps.

Obtaining CKAsset URL without downloading data

I'm using CloudKit in my app as a way to persist data remotely. One of my records has an CKAsset property that holds an image. When I'm fetching the records, I realized that it's taking so much time to finish the query. After multiple testings, I concluded that when you query records, CloutKit downloads the entire Asset file with the record object. Hence, when you obtain the Asset from the record object and request it's fileURL, it gives you a local file path URL and not an HTTP kind of URL. This is as issue to me because you have to let the user wait so much time for the entire records to be downloaded (with their associated images) before the query ends. Coming from a Parse background, Parse used to give you the HTTP URL and the query is fast enough to load the UI with the objects while loading the images async. My question is, how can I achieve what Parse does? I need to restrict the queries from downloading the Assets data and obtain a link to load the images asynchronously.
You can use CloudKit JavaScript for accessing url of asset. Here is an example;
(Used Alamofire and SwiftyJSON)
func testRecordRequest() -> Request {
let urlString = "https://api.apple-cloudkit.com/database/1/" + Constants.container + "/development/public/records/query?ckAPIToken=" + Constants.cloudKitAPIToken
let query = ["recordType": "TestRecord"]
return Alamofire.request(.POST, urlString, parameters: ["query": query], encoding: .JSON, headers: nil)
}
JSON response contains a "downloadURL" for the asset.
"downloadURL": "https://cvws.icloud-content.com/B/.../${f}?o=AmVtU..."
"${f}" seems like a variable so change it to anything you like.
let downloadURLString = json["fields"][FieldNames.image]["value"]["downloadURL"].stringValue
let recordName = json["recordName"].stringValue
let fileName = recordName + ".jpg"
let imageURLString = downloadURLString.stringByReplacingOccurrencesOfString("${f}", withString: fileName)
And now we can use this urlString to create a NSURL and use it with any image&cache solutions such as Kingfisher, HanekeSwift etc.
(You may also want to save image type png/jpg)
Make two separate calls for the same record. The first call should fetch all the NON-asset fields you want, and then second request should fetch the required assets.
Something like:
let dataQueryOperation = CKQueryOperation(query: CKQuery(predicate: myPredicate)
dataQueryOperation.desiredKeys = ["name", "age"] // etc
database.addOperation(dataQueryOperation)
let imageQueryOperation = CKQueryOperation(query: CKQuery(predicate: myPredicate)
imageQueryOperation.desiredKeys = ["images"]
database.addOperation(imageQueryOperation)
If need, refactor this into a method so you can easily make a new CKQueryOperation for every asset-containing field.
Happy hunting.
Like others have said you cannot get the url(web) of the CKAsset. So your best options are
1. Use a fetch operation with progress per individual UIImageView. I have built a custom one that shows a progress to the user. Cache is not included but you can make a class and adopt NSCoding and save the entire record to cache directory. Here you can see a fetch that i have a completion on to send the asset back to where i call it from to combine it with my other data.
// only get the asset in this fetch. we have everything else
let operation = CKFetchRecordsOperation(recordIDs: [myRecordID])
operation.desiredKeys = ["GameTurnImage"]
operation.perRecordProgressBlock = {
record,progress in
dispatch_async(dispatch_get_main_queue(), {
self.progressIndicatorView.progress = CGFloat(progress)
})
}
operation.perRecordCompletionBlock = {
record,recordID,error in
if let _ = record{
let asset = record!.valueForKey("GameTurnImage") as? CKAsset
if let _ = asset{
let url = asset!.fileURL
let imageData = NSData(contentsOfFile: url.path!)!
dispatch_async(dispatch_get_main_queue(), {
self.image = UIImage(data: imageData)
self.progressIndicatorView.reveal()
})
completion(asset!)
}
}
}
CKContainer.defaultContainer().publicCloudDatabase.addOperation(operation)
The other option is to store images on an AWS server or something comparable and then you can use something like SDWebImage to do all of the cache or heavy lifting and save a string in the CKRecord to the image.
I have asked several people about a CKAsset feature to expose a url. I don't know about the JS Api for CloudKit but there might be a way to do it with this but i will let others commment on that.
Using the Post method from #anilgoktas, we can also get the download URL without using Alamofire and SwiftyJSON, although it may be a little more complicated and not as neat.
func Request() {
let container = "INSERT CONTAINER NAME"
let cloudKitAPIToken = "INSERT API TOKEN"
let urlString = "https://api.apple-cloudkit.com/database/1/" + container + "/development/public/records/query?ckAPIToken=" + cloudKitAPIToken
let query = ["recordType": "testRecord"]
//create the url with URL
let url = URL(string: urlString)! //change the url
//create the session object
let session = URLSession.shared
//now create the URLRequest object using the url object
var request = URLRequest(url: url)
request.httpMethod = "POST" //set http method as POST
do {
request.httpBody = try JSONSerialization.data(withJSONObject: ["query": query], options: .prettyPrinted) // pass dictionary to nsdata object and set it as request body
} catch let error {
print(error.localizedDescription)
}
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
//create dataTask using the session object to send data to the server
let task = session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
guard error == nil else {
return
}
guard let data = data else {
return
}
do {
//create json object from data
if let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
DispatchQueue.main.async {
for i in json.values {
if let sub = i as? NSArray {
for j in sub {
if let dict = j as? NSDictionary, let subDict = dict["fields"] as? NSDictionary, let titleDict = subDict["title"] as? [String:String], let title = titleDict["value"], let videoDict = subDict["video"] as? NSDictionary, let vidVal = videoDict["value"] as? NSDictionary, let url = vidVal["downloadURL"] as? String {
let downloadURL = url.replacingOccurrences(of: "${f}", with: "\(title).jpg")
print(title)
print(downloadURL)
}
}
}
}
// handle json...
}
}
} catch let error {
print(error.localizedDescription)
}
})
task.resume()
}
Here is my approach.
Let's say I have Record Type : Books
BookID (auto-id or your unique id)
BookName string
BookImage CKAsset
BookURL string
Incase I use CKAssest I store in BookURL :
"Asset:\BookID.png "
Incase I used external server to store the images, I use normal URL in BookURL
"http://myexternalServer\images\BookID.png"
Queries :
with desiredKeys I query all fields without BookImage(CKAsset) field.
if BookURL is empty , there is no image for the book.
if BookURL start with "Asset:\" I query for BookImage from cloudkit
if BookURL is normal URL I download the image from external server (NSUrlSession)
This way , any time I can change and decide how to store image, on cloudkit(CKAssets) or on external server (http downloads)

Resources