Manually Throw a CKErrorPartialFailure - ios

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) { ... }
}
}

Related

Cannot fetch records from public database using cloudkit

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)

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?

Using CKCloud kit to share private records

I am using the following code to attempt to create a shared private record:
#IBAction func testPress(_ sender: Any) {
let customZone = CKRecordZone(zoneName: "ShareZone")
let friendRecord = CKRecord(recordType: "Share", zoneID: customZone.zoneID)
let rootRecord = CKRecord(recordType: "Root", zoneID: customZone.zoneID)
model.privateDB.delete(withRecordZoneID: customZone.zoneID) { (nil, error) in
self.model.privateDB.save(customZone) { (nil, error) in
print("Custom Zone Error = \(error)")
self.model.privateDB.save(friendRecord, completionHandler: { (nil, error) in
self.model.privateDB.save(rootRecord, completionHandler: { (nil, error) in
self.shareTest(record: friendRecord, root: rootRecord)
})
})
}
}
}
func shareTest(record:CKRecord, root:CKRecord) {
record["Name"] = "Test" as CKRecordValue?
root["Name"] = "Test" as CKRecordValue?
let ckContainer = CKContainer.default()
let shareRecord = CKShare(rootRecord: root, share: record.recordID)
shareRecord[CKShareTitleKey] = "Name" as CKRecordValue?
let shareController = UICloudSharingController(share: shareRecord, container: ckContainer)
shareController.delegate = self
shareController.availablePermissions = [.allowReadOnly]
self.present(shareController, animated: false)
}
However I am returning the error when I press on a way to share the link:
CKError 0x6000002535f0: "Invalid Arguments" (12); "An added share is being saved without its rootRecord (CKRecordID: 0x608000224560; recordName=C0ADC819-57F7-4D99-A527-B21590F506AB, zoneID=ShareZone:defaultOwner)"
I looked at this answer Link who was having the same problem, but do not quite know how to get their solution to work as they didn't provide enough details.
Does anyone know what I am doing wrong?
I believe the error message is telling you need to save the share and root record at the same time.
let modifyRecordsOperation = CKModifyRecordsOperation( recordsToSave: [record, share], recordIDsToDelete: nil)
You should do this operation in the completion handler of the sharingController.
sharingController = UICloudSharingController {
controller, preparationCompletionHandler in
Edit: Your code would look something like this not tested code:
#IBAction func testPress(_ sender: Any) {
let privatedatabase = CKContainer.default().privateCloudDatabase
let newZoneName = UUID().uuidString
let recordZone = CKRecordZone(zoneName: "ShareZone")
privatedatabase.save(recordZone) { savedRecordZone, error in
if let error = error {
print("\(error.localizedDescription).")
} else if let savedRecordZone = savedRecordZone {
print("\(error.localizedDescription).")
let rootRecord = CKRecord(recordType: "RootRecord", zoneID: savedRecordZone.zoneID)
rootRecord["Name"] = "Test" as CKRecordValue?
privatedatabase.save(rootRecord) { record, error in
if let error = error {
print("\(error.localizedDescription).")
} else if let record = record {
print("successfully added rootRecord to cloud.")
self.shareTest( rootRecord: record)
}
}
}
}
}
func shareTest(rootRecord:CKRecord) {
let ckContainer = CKContainer.default()
let shareRecord = CKShare(rootRecord: rootRecord)
let sharingController = UICloudSharingController { controller, preparationCompletionHandler in
let share = CKShare(rootRecord: record)
share[CKShareTitleKey] = "Share Title" as CKRecordValue
share.publicPermission = .none
let modifyRecordsOperation = CKModifyRecordsOperation( recordsToSave: [rootRecord, share], recordIDsToDelete: nil)
modifyRecordsOperation.timeoutIntervalForRequest = 10
modifyRecordsOperation.timeoutIntervalForResource = 10
modifyRecordsOperation.modifyRecordsCompletionBlock = { records, recordIDs, error in
if let error = error {
print(error.localizedDescription)
}
if let records = records {
print("Share and Root records saved successfully")
}
preparationCompletionHandler(share, CKContainer(identifier: ckContainerID ), error)
}
myCloudDatabase.add(modifyRecordsOperation)
}
if let sharingController = sharingController {
sharingController.availablePermissions = [.allowReadOnly, .allowReadWrite, .allowPrivate]
sharingController.popoverPresentationController?.sourceView = sender
sharingController.delegate = self
self.present(sharingController, animated: true)
}
}
// MARK: UICloudSharingControllerDelegate
// --------------------------------------
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
print("Failed to share to cloud: \(error)")
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return "Please join rootRecord share."
}
func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) {
print("Cloudkit stopped sharing")
}
func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) {
print("Cloudkit started sharing rootRecord")
}

Issue update a CKRecord

I can't save a record, which is already saved. recordToSave is a CKRecord download from the server. Is it possible to update it?
recordTosave.setValue("cat", forKey: "animal")
let publicData = CKContainer.defaultContainer().publicCloudDatabase
publicData.saveRecord(recordToSave, completionHandler:{(record: CKRecord?, error: NSError?) in
if error == nil{
}else{
print(error.debugDescription)
}
})
You cannot insert record which is already existing in cloudkit. You can modify the record using CKModifyRecordsOperation. Fetch the record using record ID and then update through modify operation.
let recordIDToSave = CKRecordID(recordName: "recordID")
let publicData = CKContainer.defaultContainer().publicCloudDatabase
publicData.fetchRecordWithID(recordIDToSave) { (record, error) in
if let recordToSave = record {
//Modify the record value here
recordToSave.setObject("value", forKey: "key")
let modifyRecords = CKModifyRecordsOperation(recordsToSave:[recordToSave], recordIDsToDelete: nil)
modifyRecords.savePolicy = CKRecordSavePolicy.AllKeys
modifyRecords.qualityOfService = NSQualityOfService.UserInitiated
modifyRecords.modifyRecordsCompletionBlock = { savedRecords, deletedRecordIDs, error in
if error == nil {
print("Modified")
}else {
print(error)
}
}
publicData.addOperation(modifyRecords)
}else{
print(error.debugDescription)
}
}

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