Swift CloudKit not working with Cellular (Time exceeded) - swift

I'm using following code to fetch Data from iCloud:
func fetchShoppingList() {
let container = CKContainer.default()
let publicDB = container.publicCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "ShoppingList", predicate: predicate)
let operation = CKQueryOperation(query: query)
operation.allowsCellularAccess = true
operation.qualityOfService = .userInitiated
publicDB.add(operation)
publicDB.perform(query, inZoneWith: nil) { [unowned self] results, error in
if error != nil {
print(error)
}
else {
for var value in results! {
let shoppingListEntry = ShoppingListEntry()
shoppingListEntry.index = value.value(forKey: "index") as! Int
shoppingListEntry.product = value.value(forKey: "product") as! String
shoppingListEntry.amount = value.value(forKey: "amount") as! Int
shoppingListEntry.priority = value.value(forKey: "priority") as! Int
if value.value(forKey: "isSelected") as! String == "true" {
shoppingListEntry.isSelected = true
}
else {
shoppingListEntry.isSelected = false
}
self.shoppingListEntrys.append(shoppingListEntry)
}
OperationQueue.main.addOperation({ () -> Void in
self.tableViewShoppingList.reloadData()
})
}
}
}
Everything works fine if my phone is connected via Wifi, but if I'm using Cellular I get following error: CKError 0x170244e30: "Network Failure" (4/-1001); "Zeitüberschreitung bei der Anforderung." So there seems to be a problem with time exceeding. I looked for a solution and found a post, saying I have to add the operation lines but nothing changed.
Can anybody help me please?

Related

Swift Error: containerIdentifier can not be nil (CKexception)

I'm new with Swift.
And I have my first project with CloudKit.
But unfortunately I get a error wit getting data from CloudKit:
import Foundation
import CloudKit
class CKRelation {
static let database = CKContainer.default().publicCloudDatabase
class func fetch(completion: #escaping (Result<[Relation], Error>) -> ()) {
let predicate = NSPredicate(value: true)
let name = NSSortDescriptor(key: "LastName", ascending: true)
let query = CKQuery(recordType: "Clients", predicate: predicate)
query.sortDescriptors = [name]
let operation = CKQueryOperation(query: query)
operation.desiredKeys = ["FirstName", "Initials", "LastName", "MiddleName", "RelationId"]
operation.resultsLimit = 50
var newRelations = [Relation]()
operation.recordFetchedBlock = { record in
var relation = Relation()
relation.recordID = record.recordID
relation.FirstName = record["FirstName"] as! String
relation.Initials = record["Initials"] as! String
relation.LastName = record["LastName"] as! String
relation.MiddleName = record["MiddleName"] as! String
relation.RelationId = record["RelationId"] as! Int
newRelations.append(relation)
}
operation.queryCompletionBlock = { (cursor, error) in
DispatchQueue.main.async {
if let error = error {
completion(.failure(error))
} else {
completion(.success(newRelations))
print("data")
}
}
}
database.add(operation)
}
I get this error:
*** Terminating app due to uncaught exception 'CKException', reason: 'containerIdentifier can not be nil'
terminating with uncaught exception of type CKException
I can not find anything on the internet. Hopefully somebody can help me.

Core Data - How can I get the max value from an entity attribute (Swift)

Recipe
recipeID: Int
recipeName: String
I have an entity Recipe with an attribute recipeID.
How can I get the max(recipeID) as an Int value in Swift?
I'm new in swift, please help me.
Thanks in advance.
func fetchMaxID() {
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Recipe")
fetchRequest.fetchLimit = 1
let sortDescriptor = NSSortDescriptor(key: "recipeID", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
let maxID = try [managedObjectContext?.executeFetchRequest(fetchRequest)].first
print(maxID)
} catch _ {
}
}
The way that Apple recommends and is the fastest is using NSExpressions. moc is a NSManagedObjectContext.
private func getLastContactSyncTimestamp() -> Int64? {
let request: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest()
request.entity = NSEntityDescription.entity(forEntityName: "Contact", in: self.moc)
request.resultType = NSFetchRequestResultType.dictionaryResultType
let keypathExpression = NSExpression(forKeyPath: "timestamp")
let maxExpression = NSExpression(forFunction: "max:", arguments: [keypathExpression])
let key = "maxTimestamp"
let expressionDescription = NSExpressionDescription()
expressionDescription.name = key
expressionDescription.expression = maxExpression
expressionDescription.expressionResultType = .integer64AttributeType
request.propertiesToFetch = [expressionDescription]
var maxTimestamp: Int64? = nil
do {
if let result = try self.moc.fetch(request) as? [[String: Int64]], let dict = result.first {
maxTimestamp = dict[key]
}
} catch {
assertionFailure("Failed to fetch max timestamp with error = \(error)")
return nil
}
return maxTimestamp
}
Learning from Ray Wenderlich's Core Data Tutorial
https://www.raywenderlich.com/115695/getting-started-with-core-data-tutorial/
func fetchMaxRecipe() {
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Recipe")
fetchRequest.fetchLimit = 1
let sortDescriptor = NSSortDescriptor(key: "recipeID", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
let recipes = try context.executeFetchRequest(fetchRequest) as! [Recipe]
let max = recipes.first
print(max?.valueForKey("recipeID") as! Int)
} catch _ {
}
}
Hope this helps =).
func fetchMaxID() {
let context = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let fetchRequest = NSFetchRequest(entityName: "Recipe")
fetchRequest.fetchLimit = 1
let sortDescriptor = NSSortDescriptor(key: "recipeID", ascending: false)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
let results = try context.executeFetchRequest(fetchRequest) as! [Recipe]
if (results.count > 0) {
for result in results {
print(result.recipeID!)
}
} else {
print("No Recipe")
}
} catch let error as NSError {
// failure
print("Fetch failed: \(error.localizedDescription)")
}
}
This works also!
To check the max of an entity attribute you can simply use:
func findMaxRecipeID() -> Int {
let maxRecipeID = recipe?.value(forKeyPath: "recipe.#max.recipeID") as! Int
return maxRecipeID
}
You can use this to #sum, #max, #min, #count - saves quite a few lines of code.

Querying private database in swift for user details

I'm trying to save and store the user's data, then retrieve it and check for a value, pushing the corresponding view controller.
However, despite only having 4 user records in my cloudkit dashboard, i'm getting 33 results, forcing me to change my code and preventing it from working.
This was my original code:
let container = CKContainer.defaultContainer()
let privateDB = container.privateCloudDatabase
let resultPredicate = NSPredicate(format: "TRUEPREDICATE")
let query = CKQuery(recordType: "UserData", predicate: resultPredicate)
query.sortDescriptors = [NSSortDescriptor(key: "MODIFIED", ascending: false)]
privateDB.performQuery(query, inZoneWithID: nil) { (results, error) -> Void in
if error != nil {
print("\(error)")
}
else{
for record in results! {
self.weight = record["weight"] as? Int
self.height = record["height"] as? Int
self.age = record["age"] as? Int
self.gender = record["genderFemale"] as? Int
if self.weight == nil {
print("push weightVC")
let weightVC = WeightViewController()
self.navigationController?.pushViewController(weightVC, animated: false)
}
else if self.height == nil {
print("push heightVC")
let heightVC = HeightViewController()
self.navigationController?.pushViewController(heightVC, animated: false)
}
else if self.age == nil {
print("push ageVC")
let ageVC = DOBViewController()
self.navigationController?.pushViewController(ageVC, animated: false)
}
else if self.gender == nil{
print("push genderVC")
let genderVC = GenderViewController()
self.navigationController?.pushViewController(genderVC, animated: false)
}
else{
let planVC = PlanOriginViewController()
self.navigationController?.pushViewController(planVC, animated: false)
}
I was forced to change it to this:
privateDB.performQuery(query, inZoneWithID: nil) { (results, error) -> Void in
if error != nil {
print("\(error)")
}
else{
print("\(results?.count)")
for record in results! {
self.weight = record["weight"] as? Int
self.height = record["height"] as? Int
self.age = record["age"] as? Int
self.gender = record["genderFemale"] as? Int
self.arrayOfUserData?.addObject(record)
print("record added")
}
}
}
print(arrayOfUserData)
if arrayOfUserData != nil{
let ckRecord = arrayOfUserData![0]
self.weight = ckRecord["weight"] as? Int
self.height = ckRecord["height"] as? Int
self.age = ckRecord["age"] as? Int
self.gender = ckRecord["genderFemale"] as? Int
if self.weight == nil {
print("push weightVC")
let weightVC = WeightViewController()
self.navigationController?.pushViewController(weightVC, animated: false)
}
else if self.height == nil {
print("push heightVC")
let heightVC = HeightViewController()
self.navigationController?.pushViewController(heightVC, animated: false)
}
else if self.age == nil {
print("push ageVC")
let ageVC = DOBViewController()
self.navigationController?.pushViewController(ageVC, animated: false)
}
else if self.gender == nil{
print("push genderVC")
let genderVC = GenderViewController()
self.navigationController?.pushViewController(genderVC, animated: false)
}
else{
let planVC = PlanOriginViewController()
self.navigationController?.pushViewController(planVC, animated: false)
}
} else {
}
However, this doesn't work as well. XCode is skipping over the privateDB query block and going straight to the line print(arrayOfUserData), before returning to the privateDB query.
Thanks!
You issue is that you are assuming that your file is single threaded. What you want it to evaluate What fields are not filled in once you have received a results from the database query. What you need is a completion block. This lets you query the data base and then evaluate the results after. I would do something around these lines.
first add a completion handler that gives you a user object back. I assume you have a user defined should look something like this
struct User{
weight:Int
height:Int
age:Int
gender:Sting
}
add your completion handler in your Query Method.
privateDB.performQuery(query, inZoneWithID: nil, completion:([User])) { (results, error) -> Void in
let resutltData = results.value as [String:AnyObject] ?? [:]
let users : [User] = resutltData.flatMap { record in
let weight = record["weight"] as? Int ?? ""
let height = record["height"] as? Int ?? ""
let age = record["age"] as? Int ?? ""
let gender = record["gender"] as? String ?? ""
}
completion(user)
}
now when you call privateDB.performQuery you should save it in a variable and preform your checks there. Something like this:
func ProcessUsers(){
let users = privateDB.performQuery(query, inZoneWithID: nil, completion:{ (User) in
self.user = User
switch user{
case .weight:
if .weight == "" {
print("push weightVC")
let weightVC = WeightViewController()
self.navigationController?.pushViewController(weightVC, animated: false) } else {
fallthrough
}
case .height:
if .height == "" {
print("push heightVC")
let heightVC = heightViewController()
self.navigationController?.pushViewController(heightVC, animated: false) } else {
fallthrough
}
case .age:
if .age == "" {
print("push ageVC")
let ageVC = ageViewController()
self.navigationController?.pushViewController(ageVC, animated: false) } else {
fallthrough
}
case .gender:
if .gender == "" {
print("push genderVC")
let genderVC = genderViewController()
self.navigationController?.pushViewController(genderVC, animated: false) } else {
fallthrough
}
Let me know if you have any more questions about this. I hope it helps.

CloudKit Query Operation only returns 300 results

I am currently setting up CloudKit as a replacement to Parse and need to download all of my user records. I currently have around 600 records but I am only receiving 300.
I'm using a custom record zone called "User" rather than the default "Users" record zone as this app will only ever be tied to one appID.
The code I am using is based on the answer to the below question but it's not working for me. It seems that the query operation does not run when the cursor is nil as the print(userArray) is never called. Thanks in advance for your help!
CKQuery from private zone returns only first 100 CKRecords from in CloudKit
func queryAllUsers() {
let database = CKContainer.defaultContainer().privateCloudDatabase
let query = CKQuery(recordType: "User", predicate: NSPredicate(value: true))
let queryOperation = CKQueryOperation(query: query)
queryOperation.recordFetchedBlock = self.createUserObject
queryOperation.queryCompletionBlock = { cursor, error in
if cursor != nil {
print("there is more data to fetch")
let newOperation = CKQueryOperation(cursor: cursor!)
newOperation.recordFetchedBlock = self.createUserObject
newOperation.queryCompletionBlock = queryOperation.queryCompletionBlock
database.addOperation(newOperation)
} else {
print(userArray) //Never runs
}
}
database.addOperation(queryOperation)
}
func createUserObject(record: CKRecord) {
let name = record.objectForKey("Name") as! String!
let company = record.objectForKey("Company") as! String!
let dateInductionCompleted = record.objectForKey("DateInductionCompleted") as! NSDate!
var image = UIImage()
let imageAsset = record.objectForKey("Image") as! CKAsset!
if let url = imageAsset.fileURL as NSURL? {
let imageData = NSData(contentsOfURL:url)
let mainQueue = NSOperationQueue.mainQueue()
mainQueue.addOperationWithBlock() {
image = UIImage(data: imageData!)!
userArray.append(User(name: name, company: company, image: image, dateInductionCompleted: dateInductionCompleted))
}
}
print(userArray.count)
}
UPDATE
The question has been answered, it was possibly an inherent bug when using a cursor for large queries. The code now works by using a recursive function, working code below:
func queryRecords() {
let database = CKContainer.defaultContainer().privateCloudDatabase
let query = CKQuery(recordType: "User", predicate: NSPredicate(value: true))
let queryOperation = CKQueryOperation(query: query)
queryOperation.qualityOfService = .UserInitiated
queryOperation.recordFetchedBlock = populateUserArray
queryOperation.queryCompletionBlock = { cursor, error in
if cursor != nil {
print("There is more data to fetch")
self.fetchRecords(cursor!)
}
}
database.addOperation(queryOperation)
}
func fetchRecords(cursor: CKQueryCursor?) {
let database = CKContainer.defaultContainer().privateCloudDatabase
let queryOperation = CKQueryOperation(cursor: cursor!)
queryOperation.qualityOfService = .UserInitiated
queryOperation.recordFetchedBlock = populateUserArray
queryOperation.queryCompletionBlock = { cursor, error in
if cursor != nil {
print("More data to fetch")
self.fetchRecords(cursor!)
} else {
print(userArray)
}
}
database.addOperation(queryOperation)
}
func populateUserArray(record: CKRecord) {
let name = record.objectForKey("Name") as! String!
let company = record.objectForKey("Company") as! String!
let dateInductionCompleted = record.objectForKey("DateInductionCompleted") as! NSDate!
var image = UIImage()
let imageAsset = record.objectForKey("Image") as! CKAsset!
if let url = imageAsset.fileURL as NSURL? {
let imageData = NSData(contentsOfURL:url)
let mainQueue = NSOperationQueue.mainQueue()
mainQueue.addOperationWithBlock() {
image = UIImage(data: imageData!)!
userArray.append(User(name: name, company: company, image: image, dateInductionCompleted: dateInductionCompleted))
}
}
print(userArray.count)
}
Could you try setting:
queryOperation.qualityOfService = .UserInitiated
This will indicate that your user interaction requires the data.
Otherwise it could happen that de request is ignored completely.
As discussed below the actual answer was that you should not re-use completion blocks. Instead you should create a recursive function for fetching the next records from a cursor. A sample of that can be found at: EVCloudKitDao

How can I speed up performQuery in Swift?

So I am creating this app that uses CloudKit to save and fetch images and text from the Cloud. The problem is that I can only access the results after the whole fetch is done. I would like to be able to fetch each record individually as it is fetched. Here is the code.
func fetchPost() {
spinner.startAnimating()
if imageView.image != nil {
spinner.alpha = 0
}
var imageData = [UIImage]()
var text = [String]()
let predicate = NSPredicate(value: true)
let sort = NSSortDescriptor(key: "creationDate", ascending: false)
let query = CKQuery(recordType: "Post",
predicate: predicate)
query.sortDescriptors = [sort]
publicDB.performQuery(query, inZoneWithID: nil) {
results, error in
if error != nil {
dispatch_async(dispatch_get_main_queue()) {
println("Query failed")
return
}
} else {
println("test")
var number = 0
for record in results {
if let pictureRecord = record as? CKRecord {
let post = Post(record: pictureRecord, database: self.publicDB)
let postImageData = post.imageData
let postText = post.text
self.images.append(UIImage(data: postImageData)!)
self.texts.append(postText)
println("\"\(postText)\" is the text. Fetch successful.")
if number == 0 {
self.imageView.image = self.images[0]
self.nameLabel.text = self.texts[0]
}
++number
} else {
println("Records failed")
}
}
}
self.spinner.stopAnimating()
self.spinner.alpha = 1
}
}
Thanks!
If you use CKQueryOperation you can set a callback (recordFetchedBlock) that will be called for each record as it is fetched from the server.