Cannot fetch records from public database using cloudkit - ios

This is a method I call to fetch public records:
private func fetchPublicRecordZonesChanges(completion: ErrorHandler?) {
let zone = CKRecordZone.default()
let options = CKFetchRecordZoneChangesOptions()
options.previousServerChangeToken = nil
var records = [CKRecord]()
var recordIDsToDelete = [CKRecordID]()
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: [zone.zoneID], optionsByRecordZoneID: [zone.zoneID: options])
operation.database = CloudAssistant.shared.publicDatabase
operation.recordChangedBlock = { record in
records.append(record)
}
operation.recordWithIDWasDeletedBlock = { recordID, string in
recordIDsToDelete.append(recordID)
}
operation.recordZoneChangeTokensUpdatedBlock = { _, token, _ in
if let token = token {
Token.temporaryPublicZoneServerChangeToken = token
}
}
operation.recordZoneFetchCompletionBlock = { [weak self] _, token, _, _, error in
if let error = error, error.isTokenExpiredError {
UserDefaults.remove(forKey: PublicZoneServerChangeTokenKey)
self?.fetchPublicRecordZonesChanges(completion: completion)
return
}
if let token = token {
Token.temporaryPublicZoneServerChangeToken = token
}
}
operation.fetchRecordZoneChangesCompletionBlock = { [weak self] error in
self?.save(records: records, recordIDsToDelete: recordIDsToDelete) { error in
completion?(error)
}
}
operationQueue.addOperation(operation)
}
But nothing is fetched although that method is called, additionally not even once recordChangedBlock closure is called. Why?
I am pretty sure I have a records there:
and environment is also very fine, because private development records are fetched correctly (using of course different method). What am I doing wrong?

To be notified of changes to your Public database, create a CKQuerySubscription for the record type(s) you care about. Here's an example:
let subscription = CKQuerySubscription(
recordType: "Question",
predicate: NSPredicate(value: true),
subscriptionID: "subscriptionQuestion",
options: [
.firesOnRecordCreation,
.firesOnRecordUpdate,
.firesOnRecordDeletion
])
let info = CKNotificationInfo()
info.shouldSendContentAvailable = true
subscription.notificationInfo = info
let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: nil)
operation.modifySubscriptionsCompletionBlock = { saved, deleted, error in
if let error = error{
print("Add subscription error: \(error)")
}else{
print("Successfully added Question subscription.")
}
}
//:::
let container = CKContainer(identifier: "...")
container.publicCloudDatabase.add(operation)

Related

Manually Throw a CKErrorPartialFailure

I'm tracking down a CloudKit error of 'Failed to modify some records.'
How can I throw this error so that I can test my error handling code?
Is there a property of CKRecord I can set to force it to fail?
Code is currently something like:
var someRecords = [CKRecord]()
for i in (1...10) {
let record = CKRecord(recordType: "Track", recordID: CKRecord.ID(zoneID: recordZone.zoneID))
...
someRecords.append(record)
}
let operation = CKModifyRecordsOperation(recordsToSave: someRecords, recordIDsToDelete: nil)
operation.modifyRecordsCompletionBlock = { savedRecords, deletedRecords, error in
DispatchQueue.main.async {
if !self.handleError(error) { ... }
}
}
You need an Atomic zone (as I remember)
Try to save a big amount of objects (as I remember: it will allow you to use no more < 500)
I hope this will help you
var someRecords = [CKRecord]()
for i in (1...1000) {
let record = CKRecord(recordType: "Track", recordID: CKRecord.ID(zoneID: recordZone.zoneID))
...
someRecords.append(record)
}
let operation = CKModifyRecordsOperation(recordsToSave: someRecords, recordIDsToDelete: nil)
operation.isAtomic = true
operation.modifyRecordsCompletionBlock = { savedRecords, deletedRecords, error in
DispatchQueue.main.async {
if !self.handleError(error) { ... }
}
}

Can't save to Cloudkit public database after saving to custom zone in private database

Good evening,
I have run into the issue of getting the following error message from Cloudkit when trying to save a record to the public database:
"Server Rejected Request" (15/2027); server message = "Custom zones are not allowed in public DB";
I have been able to find the issue causing this error. Within this application I need to fetch zone changes on the private database so I have had to save my records to a custom zone in order to accomplish that fetch.
The following code is where I am saving to the custom zone on the private database:
let zoneID = CKManager.defaultManager.sharedZone?.zoneID
let deckSetToBeSaved = deckName.deckToCKRecord(zoneID)
privateDatabase.save(deckSetToBeSaved) { (record, error) in
DispatchQueue.main.async {
if let record = record {
deckName.cKRecordToDeck(record)
try? self.managedContext.save()
}
}
print("New record saved")
print(error as Any)
}
}
Here is my code for saving a record to the public database:
func shareDeckPlan(_ deck: Deck, completion: #escaping (Bool, NSError?) -> Void ) {
var records = [CKRecord]()
let deckRecord = deck.deckToCKRecord()
records.append(deckRecord)
let reference = CKReference(record: deckRecord, action: .deleteSelf)
for index in deck.cards {
let cardRecord = index.cardToCKRecord()
cardRecord["deck"] = reference
records.append(cardRecord)
}
let operation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
operation.modifyRecordsCompletionBlock = {(savedRecords, deletedIDs, error) in
if error == nil {
DispatchQueue.main.async(execute: {
if let savedRecords = savedRecords {
for record in savedRecords {
if record.recordID.recordName == deck.ckRecordID {
deck.cKRecordToDeck(record)
}
for cards in deck.cards {
if record.recordID.recordName == cards.ckRecordID {
cards.ckRecordToCard(record)
}
}
}
}
let _ = try? self.managedContext.save()
DispatchQueue.main.async(execute: {
completion(true, error as NSError?)
})
})
} else {
DispatchQueue.main.async(execute: {
print(error!)
completion(false, error as NSError?)
})
}
}
operation.qualityOfService = .userInitiated
self.publicDatabase.add(operation)
}
The following code referes to the deckToCKRecord method:
func deckToCKRecord(_ zoneID:CKRecordZoneID? = nil) -> CKRecord {
let deckSetName: CKRecord
if let ckMetaData = ckMetaData {
let unarchiver = NSKeyedUnarchiver(forReadingWith: ckMetaData as Data)
unarchiver.requiresSecureCoding = true
deckSetName = CKRecord(coder: unarchiver)!
}
else {
if let zoneID = zoneID {
deckSetName = CKRecord(recordType: "Deck", zoneID: zoneID)
self.ckRecordID = deckSetName.recordID.recordName
} else {
deckSetName = CKRecord(recordType: "Deck")
self.ckRecordID = deckSetName.recordID.recordName
}
}
deckSetName["name"] = name! as CKRecordValue
deckSetName["cardCount"] = cards.count as CKRecordValue
return deckSetName
}
How can I save a record both to the private database in a custom zone and the public database successfully?

CloudKit private database returns first 100 CKRecords

i am facing this kind of problem working with CloudKit. Trying to fetch all data from "Data" record. Result is limited by 100. How to get all data? Please, thank for any advice.
func getAllDataFromCloudKit(){
let predicate = NSPredicate(value: true)
let container = CKContainer.defaultContainer()
let privateDatabase = container.privateCloudDatabase
let query = CKQuery(recordType: "Data", predicate: predicate)
privateDatabase.performQuery(query, inZoneWithID: nil) { results, error in
if error != nil {
print(error)
}
else {
for result in results! {
// return only 100 first
}
}
}
}
P.S. i found one similar question, still not clear or answer is too old and does not work with the new Swift version
EDIT: See my final solution how to get all data from private database below:
Ok, i found a solution. See below:
func loadDataFromCloudKit() {
var results: [AnyObject] = []
let cloudContainer = CKContainer.defaultContainer()
let privateDatabase = cloudContainer.privateCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Data", predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
let queryOperation = CKQueryOperation(query: query)
queryOperation.desiredKeys = ["id","name"]
queryOperation.queuePriority = .VeryHigh
// Max limit is still 100
queryOperation.resultsLimit = 100
queryOperation.recordFetchedBlock = { (record:CKRecord!) -> Void in
results.append(record)
}
queryOperation.queryCompletionBlock = { (cursor, error) in
dispatch_async(dispatch_get_main_queue()) {
if (error != nil) {
print("Failed to get data from iCloud - \(error!.localizedDescription)")
} else {
print("Successfully retrieve the data form iCloud")
}
}
// see cursor here, if it is nil than you have no more records
// if it has a value than you have more records to get
if cursor != nil {
print("there is more data to fetch")
let newOperation = CKQueryOperation(cursor: cursor!)
newOperation.recordFetchedBlock = { (record:CKRecord!) -> Void in
results.append(record)
}
newOperation.queryCompletionBlock = queryOperation.queryCompletionBlock
privateDatabase.addOperation(newOperation)
} else {
// gets more then 100
print(results.count)
}
}
privateDatabase.addOperation(queryOperation)
}
Kevin,
You use the cursor returned by CKQueryOperation; it is very much a standard approach in the world of databases; I know some dropbox operations use the same approach for example. Here is the basic code for a CKQueryOperation.
func query4Cloud(theLink: String, theCount: Int) {
var starCount:Int = 0
let container = CKContainer(identifier: "iCloud.ch")
let publicDB = container.publicCloudDatabase
let singleLink2LinkthemALL = CKRecordID(recordName: theLink)
let recordToMatch = CKReference(recordID: singleLink2LinkthemALL, action: .DeleteSelf)
let predicate = NSPredicate(format:"theLink == %#", recordToMatch)
let query = CKQuery(recordType: "Files", predicate: predicate)
// You run the query operation replacing query with your cursor
readerOperation = CKQueryOperation(query: query)
readerOperation.desiredKeys = ["record.recordID.recordName"];
readerOperation.recordFetchedBlock = { (record) in
starCount += 1
}
// see cursor here, if it is nil than you have no more records
// if it has a value than you have more records to get
readerOperation.queryCompletionBlock = {(cursor, error) in
print("fcuk query4Cloud \(theLink) \(theCount) \(starCount)" )
if error != nil {
self.showAlert(message: error!.localizedDescription)
print("ting, busted")
} else {
// it's done
}
}
print("publicDB.addOperation(operation)")
readerOperation.qualityOfService = .Background
publicDB.addOperation(readerOperation)
}

CloudKit saveRecord Complete

I am using the code below to update a record. Once the record has been updated I would like to run my refresh function. At the moment the refresh function is sometimes called before the record has been updated so the refresh results are the same as before the record was updated.
Thanks
var tempDocumentsArray:NSArray!
let recordID = CKRecordID(recordName: "layerAbove")
var predicate = NSPredicate(format: "recordID = %#", recordID)
let query = CKQuery(recordType: "Layers", predicate: predicate)
self.publicDB.performQuery(query, inZoneWithID: nil) { (results, error) -> Void in
tempDocumentsArray = results
print("Results are: \(tempDocumentsArray)")
let record = tempDocumentsArray[0] as! CKRecord
var layerAbovePrevPos = record.objectForKey("layerNumber") as! Int
layerAbovePrevPos = layerAbovePrevPos - 1
let nlnChanged = record.setObject(layerAbovePrevPos, forKey: "layerNumber")
self.publicDB.saveRecord(record, completionHandler: { (returnRecord, error) -> Void in
if let err = error {
print("Error: \(err.localizedDescription)")
} else {
dispatch_async(dispatch_get_main_queue()) {
print("Success")
//TODO:This is sometimes called before the save is complete!
self.resetAndGet()
}
}
})
}

CKQueryOperation error: This operation has been rate limited

Client received error 1298: This operation has been rate limited error from CloudKit when downloading records with CKQueryOperation, only once, during Apple review. How can I fix this issue?
Here is to code, nothing special:
let query = CKQuery(recordType: "Movie", predicate: NSPredicate(format: "creationDate > %#", d!))
let qo = CKQueryOperation(query: query)
let fb: (CKRecord!) -> () = {record in
temporaryContext.performBlockAndWait({
let fr = NSFetchRequest(entityName: "Movie")
fr.predicate = NSPredicate(format: "recordName = %#", record.recordID.recordName)
let a = temporaryContext.executeFetchRequest(fr, error: nil) as! [Movie]
if a.count == 0 {
let m = NSEntityDescription.insertNewObjectForEntityForName("Movie", inManagedObjectContext: temporaryContext) as! Movie
m.title = record.valueForKey("title") as! String
m.image = (record.valueForKey("image") as! CKAsset).fileURL.description
m.imageSize = Int32(record.valueForKey("imageSize") as! Int)
m.recordName = record.recordID.recordName
}
})
}
let c: ()->() = {
temporaryContext.performBlockAndWait({
let success = temporaryContext.save(nil)
})
Utility.managedObjectContext().performBlockAndWait({
let success = Utility.managedObjectContext().save(nil)
})
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "moviesDownloaded")
NSUserDefaults.standardUserDefaults().synchronize()
dispatch_semaphore_signal(self.sema)
}
let cb: (CKQueryCursor!, NSError!) -> () = {cursor, error in
if error == nil {
if cursor != nil {
let qo2 = Utility.qo(cursor, recordFetchedBlock: fb, completion: c)
publicDatabase.addOperation(qo2)
} else {
c()
}
} else {
Utility.log("error 1298: \(error.localizedDescription)")
dispatch_sync(dispatch_get_main_queue(), {
self.status.backgroundColor = UIColor.orangeColor()
})
NSThread.sleepForTimeInterval(0.5)
dispatch_semaphore_signal(self.sema)
}
}
qo.recordFetchedBlock = fb
qo.queryCompletionBlock = cb
publicDatabase.addOperation(qo)
dispatch_semaphore_wait(self.sema, DISPATCH_TIME_FOREVER)
I try to put this whole code into a loop like:
for i in 1 ... 2 {
var rateLimited = false
...
if error == nil {
} else {
NSThread.sleepForTimeInterval(3)
rateLimited = true
}
...
if !rateLimited {
break
}
}
Do you think it will work?
If you get CKErrorRequestRateLimited the error will have a CKErrorRetryAfterKey key in the error's userInfo dictionary. You should wait at least that amount of time before retrying your operation.
Waiting with a sleep is a bad idea because it can cause unexpected hangs in your application, especially if that code runs on your main thread. Use dispatch_after or a NSTimer to re-send your operation.
You will also get this error if you are not logged in to your iCloud account.

Resources