Obtaining CKAsset URL without downloading data - ios

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)

Related

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

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()

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

Sending images from ios to server/database

I have an app where user selects 5 imagesfrom photo library, then i send to send those 5 UIImages to a nodejs server and store them in a mysql database, however i am getting many issues with this approach when i fetch the data from the database and try to convert back to uiimage, also many people here have said that its better to store the image url or path to image in the database instead of the actual image , so i have decided to try that approach however i do not know how to get an image url? Or how to get the path of the image? My app still needs to send an image from the iOS device, to nodejs server, store the actual image, and retrieve the images so other users can see it but where and how do i store the image?
UPDATE: so my issue is I start with a UIImage in swift, convert it to base64 string representation, send to nodejs server, and insert it into mysql database, but when i send the base64 string representation from the mysql table back to the ios device i get an error trying to convert the string back to a uiimage, i convert the string back to Data object and then try to create a uiimage from that data object but the uiimage is always nil, and i dont get an error description from xcode so i do not know how to go about this?
func sendToNodeServer()
{
let url: URL = URL(string: "http://localhost:8081/testInsert")!
let session = URLSession.shared
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringCacheData
let imageData:NSData = UIImagePNGRepresentation(imV.image!)! as NSData
let strBase64:String = imageData.base64EncodedString(options: .lineLength64Characters)
let paramString = "pic=" + strBase64
request.httpBody = paramString.data(using: String.Encoding.utf8)
let task = session.dataTask(with: request)
{ data, response, error in
if error != nil
{
print("error in web request")
}
else
{
}
}//completion handler end
task.resume() //start the web reuqest
}
func getFromNodeServer()
{
let url: URL = URL(string: "http://localhost:8081/testgrab")!
let session = URLSession.shared
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.cachePolicy = NSURLRequest.CachePolicy.reloadIgnoringCacheData
let task = session.dataTask(with: request)
{ data, response, error in
if error != nil
{
print("error in web request")
}
else
{
DispatchQueue.main.async
{
self.parseWeb2(stuff: data) //stoer results in class member
}
}
}//completion handler end
task.resume() //start the web reuqest
}
func parseWeb2(stuff: Data?)
{
if stuff != nil
{
if let dataAsAny: NSArray = try? JSONSerialization.jsonObject(with: stuff!, options: .mutableContainers) as! NSArray
{
let dic: [String: AnyObject] = dataAsAny[0] as! [String: AnyObject]
let str: String = dic["value"] as! String
let data: NSData = NSData(base64Encoded: str, options: NSData.Base64DecodingOptions.ignoreUnknownCharacters)!
//ERROR, UIImage is always nil
let p: UIImage = UIImage(data: data as Data)!
imV.image = p
}
}
}
//nodejs code
//import modules
var path = require("path");
var bodyParser = require('body-parser');
var express = require('express');
var mysql = require("mysql");
var fs = require('fs');
var app = express();
app.use(bodyParser.json({limit: "50mb"}));
app.use(bodyParser.urlencoded({limit: "50mb", extended: true, parameterLimit:50000}));
var con = mysql.createConnection(
{ /*my credentials*/ } );
app.post('/testInsert', function (req, res)
{
var queryString = "Insert into Test(value) VALUES ( ? );";
con.query(queryString,[req.body.pic],function(err,rows)
{
if(err) throw err;
});//CON.QUERY end
});
app.post('/testgrab', function (req, res)
{
var queryString = "select test.value from test where id = 3;";
con.query(queryString,function(err,rows)
{
if(err) throw err;
res.send(rows);
});//CON.QUERY end
});
//mysql has 1 table called test with 2 colums (id,value) of type (int, medium text)
I solved my problem, the issue was that base 64 string contains the characters +/= which aren't allowed in URL encoding so i had to convert my base 64 string to correct url encoded format then i upload the string and can retrieve it correctly, to url encode the string i added percent escapes for the +=/ chars

How to get userID from JSON response while JSON text did not start with array or object and option to allow fragments not set

I got json response from server like this:
"{\"userID\":\"dkjagfhaghdalgalg\"}"
I try to get that userID with this:
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
(data, response, error) -> Void in
if let unwrappedData = data {
do {
let userIDDictionary:NSDictionary = try NSJSONSerialization.JSONObjectWithData(unwrappedData, options: NSJSONReadingOptions.MutableContainers) as! NSDictionary
print("userIDDictionary:\(userIDDictionary)")
//let userID:String = userIDDictionary["userID"] as! String
//print("userID:\(userID)")
print("data:\(data)")
print("response:\(response)")
print("error:\(error)")
} catch {
print("Failed to get userID: \(error)")
}
}
}
but the response is
Failed to get userID: Error Domain=NSCocoaErrorDomain Code=3840 "JSON text did not start with array or object and option to allow fragments not set. UserInfo={NSDebugDescription=JSON text did not start with array or object and option to allow fragments not set.}".
How to get userID with json response like that?
update: I try to get with anyobject but still did not get that json string to change to dictionary.
let bodyStr = "test={ \"email\" : \"\(username)\", \"password\" : \"\(password)\" }"
let myURL = NSURL(string: Constant.getSignInEmail())!
let request = NSMutableURLRequest(URL: myURL)
request.HTTPMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.HTTPBody = bodyStr.dataUsingEncoding(NSUTF8StringEncoding)!
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
(data, response, error) -> Void in
if let unwrappedData = data {
do {
let json:AnyObject! = try NSJSONSerialization.JSONObjectWithData(unwrappedData, options: NSJSONReadingOptions.MutableContainers) as! AnyObject
print("json:\(json)")
//let userID:String = userIDDictionary["userID"] as! String
//print("userID:\(userID)")
} catch {
print("Failed to get userID: \(error)")
}
}
}
Try with try with NSJSONReadingOptions.AllowFragments in json reading options
I think this is a case of confusion between the data that you are receiving and the way they are displayed. Either on your side, or on the server side. Try to tell us exactly what the server is sending, byte for byte.
What you have got there is a string containing JSON. Not JSON, but a string containing JSON. Which is not the same. Just like a bottle of beer is not made of beer, but of glass.
If this is indeed what you are receiving, then you should first kick the guys writing the server code. If that doesn't help, then read what the "fragment" option does; this will give you a string, then you extract the bytes, and throw the bytes into a JSON parser.
Two way you can resolve.
Check your Webservice format and correct it as {"key":"value","key":"value"}
or else you have to Convert NSData to NSString.
Using String(data:, encoding:.utf8)
then format the string file with reduction '\'
then again convert it to NSData type then Call JSONSerialization.
Actually this is NSASCIIStringEncoding.
For help, I created a program.
Please just copy/paste and run it. You will find your answer.
import Foundation
let string = "{\"userID\":\"dkjagfhaghdalgalg\"}"
let unwrappedData: NSData = string.dataUsingEncoding(NSASCIIStringEncoding)!
do {
let userIDDictionary:NSDictionary = try NSJSONSerialization.JSONObjectWithData(unwrappedData, options: NSJSONReadingOptions.MutableContainers) as! NSDictionary
let userid = userIDDictionary.valueForKey("userID")
print("userid:\(userid!)")
} catch {
print("Failed to get userID: \(error)")
}

JSON parsing and returning data in Swift

i have a Swift code for retrieving and parsing JSON data from the web. everything seems to work fine except one problem i am facing right now. I tried to solve it for some time, have check all sources online.
I have created global dictionary "dicOfNeighbours" that would like to return as a result of parse to other class by calling "func startConnection".
dicOfNeighbours stores parsed data till it goes out of the closing bracket of the:
"let task = session.dataTaskWithRequest(urlRequest) { ... }"
After it stores just nil. And returned result is nil as well.
I have tried to pass "dicOfNeighbours" variable by reference using inout and it is still returns nil result.
there might some solution that i missed.
I would appreciate any help and suggestions.
Thanks!
var dicOfNeighbours = Dictionary<String, [Int]>()
func startConnection() -> Dictionary<String, [Int]>{
let requestURL: NSURL = NSURL(string: "http://www....data.json")!
let urlRequest: NSMutableURLRequest = NSMutableURLRequest(URL: requestURL)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(urlRequest) {
(data, response, error) -> Void in
let httpResponse = response as! NSHTTPURLResponse
let statusCode = httpResponse.statusCode
if (statusCode == 200) {
do{
let json = try NSJSONSerialization.JSONObjectWithData(data!, options:.AllowFragments)
if let neighbours = json["neighbours"] as? [String: Array<Int>] {
var i = 0
for (index, value) in neighbours {
self.dicOfNeighbours[index] = value
}
}
}catch {
print("Error with Json: \(error)")
}
}
}
task.resume()
return self.dicOfNeighbours
}
You are using return instead of using a callback. You are doing your parsing when the network connection is done; asynchronously.
To synchronize it, you'd need to use semaphores, but that is highly discouraged on the main thread.
Instead, do the appropriate things with the result when your completion block is executed. Think of the data task as 'do stuff, come back to me when you're done'.

Resources