In iOS 10 my app's CloudKit features don't work anymore. The exact same app works fine on iOS 9. I tried building in XCode 8 and it still doesn't work.
The code that doesn't work, and the error it generates, is shown below. What we do is get a record from the public cloud database. I have confirmed the device has a fresh iCloud account on it. The same device worked perfectly with the app under iOS 9. I tried restarting the device and signing in and out of iCloud, but still get the same error.
Please advise...
CKDatabase *publicDatabase = [[CKContainer defaultContainer] publicCloudDatabase];
CKRecordID *myRecordID = [[CKRecordID alloc] initWithRecordName:@"myRecord"];
[publicDatabase fetchRecordWithID:myRecordID completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
if(error != nil) {
CLS_LOG(@"<ERROR> Error fetching record. Error: %@",error);
return;
}
//rest of code
}];
Results in:
Error: <CKError 0x15e7c2b0: "Internal Error" (1/5001); "Couldn't get a
PCS object to unwrap encrypted data for field
encryptedPublicSharingKey: (null)">
I have been in communication with Apple's Developer Technical Support Team on this issue for several weeks. As @ewcy's answer alludes, the problem is related to the Short GUID property. I would have set that as the correct answer, however, there is a bit more to it than that.
As of iOS 10.0.2, if the record is in the public database, and it has the Short GUID option checked, then the following error will be encountered when an iOS 10 device tries to fetch the record:
CKError 0x15e7c2b0: "Internal Error" (1/5001); "Couldn't get a PCS object to unwrap encrypted data for field encryptedPublicSharingKey: (null)"
However if the record is in the private database (regardless of whether it's in a custom zone or the default zone), then it can have a Short GUID and still be fetched just fine on iOS 10.
Apple has told me this inability to fetch records with Short GUIDs from the public DB is a bug in iOS 10 and I am sure they will fix in some future update. They could not tell me, however, how long it would be until this is fixed.
Workaround: So, at least for now, the workaround (that @ewcy also mentioned) is to recreate all the records in your public database without the Short GUID option checked. The way I did it was just to create the records directly in the CloudKit dashboard. The way @ewcy did it was using JavaScript. You can also do it in Objective C, as I show in the code example below. Lastly, you can do it in Swift (especially if you like exclamation marks and question marks a lot!!!??!!?!1).
The Short GUID property is related to the new sharing features of CloudKit that were introduced in iOS 10. If you programmatically add a CKShare to a CKRecord (which can only be done on records in a custom zone in the private database, unless you want to crash your app... heh) then that record will automatically get a Short GUID. You can test this with the sample code below.
The bug probably comes from the fact that public DB records cannot be shared, hence why their encryptedPublicSharingKey
is null. But without being able to see behind the curtain of Apple's APIs, it's hard to know for sure what the exact reason is.
Apple's Developer Tech Support person who helped me with this case told me they should not even have the option to set the Short GUID on any records for which sharing is impossible, like those records in the public database. It's pointless to even have that option there.
Of course, when most of us developers see "Short GUID", we think, "GUIDs are cool, I better check that box! Might need that GUID later!" I mean, who doesn't love GUIDs, amirite? ¯_(ツ)_/¯
Also note that there's a different bug on iOS 10 where if iCloud Drive is not enabled, or no one is signed into iCloud on the device, then basically doing anything in CloudKit fails (but with different error messages). All that stuff worked fine on iOS 9 however.
Maybe we will see some updates to all these problem in iOS 10.1? The fact is that CloudKit is partially broken on iOS 10 and has been for well over a month now. I realize I am not supposed to editorialize in this comment, so I won't, but lets just say I'm showing a lot of restraint.
Sample code to isolate when and where the problem will actually happen (also reveals the other iOS 10 CloudKit bugs that I mentioned, if you have iCloud Drive turned off or you're not signed into iCloud on the device):
//
// AppDelegate.m
//
#import "AppDelegate.h"
#import <CloudKit/CloudKit.h>
#define LOG(__FORMAT__, ...) NSLog(@"\n\n" __FORMAT__ @"\n\n", ##__VA_ARGS__)
@interface AppDelegate()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
CKDatabase *publicDatabase = [[CKContainer defaultContainer] publicCloudDatabase];
/* First create public testRecord_noGuid with no Short GUID, and testRecord_withGuid with a Short GUID, in the CloudKit Dashboard. Then run this code. */
/* Try to fetch the one without a Short GUID. */
CKRecordID *testRecordID_noGuid = [[CKRecordID alloc] initWithRecordName:@"testRecord_noGuid"];
[publicDatabase fetchRecordWithID:testRecordID_noGuid completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
if(error != nil) {
LOG(@"<ERROR> Error fetching testRecord_noGuid from public default DB. Error: %@",error);
return;
}
/* This is where we end up on iOS 9 or 10. */
NSDictionary *middle = [record dictionaryWithValuesForKeys:record.allKeys];
LOG(@"<NOTICE> testRecord_noGuid fetched from public default zone: %@",middle);
}];
/* Try to fetch the one with a Short GUID. */
CKRecordID *testRecordID_withGuid = [[CKRecordID alloc] initWithRecordName:@"testRecord_withGuid"];
[publicDatabase fetchRecordWithID:testRecordID_withGuid completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
if(error != nil) {
/* This is where we always end up on iOS 10. */
LOG(@"<ERROR> Error fetching testRecord_withGuid from public default zone. Error: %@",error);
return;
}
/* This is where we end up on iOS 9. */
NSDictionary *middle = [record dictionaryWithValuesForKeys:record.allKeys];
LOG(@"<NOTICE> testRecord_withGuid fetched from public default zone: %@",middle);
}];
/* The below code demonstrates the issue does not affect private records in custom zones.
First time you run this, do it while logged into your developer AppleID with permissions to create a new zone.
That way you can login to CloudKit dashboard and verify whether the created records have a Short GUID or not.
*/
CKDatabase *privateDB = [[CKContainer defaultContainer] privateCloudDatabase];
CKRecordZone *testRecordZone = [[CKRecordZone alloc] initWithZoneName:@"TestRecordZone"];
/* Create a new zone */
[privateDB saveRecordZone:testRecordZone completionHandler:^(CKRecordZone * _Nullable zone, NSError * _Nullable error) {
if(error != nil) {
LOG(@"<ERROR> Error saving private custom zone. Error: %@", error);
[self checkPrivateRecordCreationInZone:testRecordZone]; // This will only work if the zone was already created.
return;
}
LOG(@"<NOTICE> Saving of TestRecordZone succeeded. Proceeding to create records in it.");
/* Now that the new zone is created, create a test record without a Short GUID */
[self checkPrivateRecordCreationInZone:zone];
}];
/* Uncomment this and run it if the testRecordZone is already created and now you are testing from an
AppleID without any perms. */
[self checkPrivateRecordCreationInZone:testRecordZone];
/* Run this to see how it works int he default zone */
//[self checkPrivateRecordCreationInZone:[CKRecordZone defaultRecordZone]];
return YES;
}
- (void)checkPrivateRecordCreationInZone:(CKRecordZone *)zone {
CKDatabase *privateDB = [[CKContainer defaultContainer] privateCloudDatabase];
CKRecordID *testRecordID_noGuid_private = [[CKRecordID alloc] initWithRecordName:@"testRecord_noGuid" zoneID:zone.zoneID];
CKRecord *testRecord_noGuid_private = [[CKRecord alloc] initWithRecordType:@"TestRecordType" recordID:testRecordID_noGuid_private];
[testRecord_noGuid_private setValue:@"foo" forKey:@"bar"];
[privateDB saveRecord:testRecord_noGuid_private completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
if(error != nil) {
if([error.userInfo[@"ServerErrorDescription"] containsString:@"already exists"]) {
record = error.userInfo[@"ServerRecord"];
LOG(@"<NOTICE> Record with no Short GUID already existed in private zone: %@",record);
}
else {
if(error.userInfo[@"CKRetryAfter"] != nil) {
/* Retry after three seconds :D */
double delay = [error.userInfo[@"CKRetryAfter"] doubleValue] + 0.1;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self checkPrivateRecordCreationInZone:zone];
});
}
LOG(@"<ERROR> Error saving record with no Short GUID to private custom zone. Error: %@", error);
return;
}
}
else {
LOG(@"<NOTICE> Created new record with no Short GUID in private custom zone: %@",record);
}
/* Now that we successfully created a record without a short GUID to the private store, fetch it. */
[privateDB fetchRecordWithID:testRecordID_noGuid_private completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
if(error != nil) {
LOG(@"<ERROR> Error fetching testRecord_noGuid from private custom zone: %@",error);
return;
}
LOG(@"<NOTICE> Successfully fetched testRecord_noGuid from private custom zone: %@",record);
}];
}];
/* On iOS 10 or later we can create a private record with a share to force it to have a Short GUID.
On iOS 9 or earlier you have to manually create the private record with Short GUID after running
the test once to create the TestCustomZone and the private record with no Short GUID. */
CKRecordID *testRecordID_withGuid_private = [[CKRecordID alloc] initWithRecordName:@"testRecord_withGuid" zoneID:zone.zoneID];
CKRecord *testRecord_withGuid_private = [[CKRecord alloc] initWithRecordType:@"TestRecordType" recordID:testRecordID_withGuid_private];
if([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 10 || [zone isEqual:[CKRecordZone defaultRecordZone]]) {
[privateDB fetchRecordWithID:testRecordID_withGuid_private completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
if(error != nil) {
LOG(@"<ERROR> Error fetching testRecord_withGuid from private custom zone: %@",error);
return;
}
LOG(@"<NOTICE> Successfully fetched testRecord_withGuid from private custom zone: %@",record);
}];
return;
}
/* Create a test record with a Short GUID */
[testRecord_withGuid_private setValue:@"foo" forKey:@"bar"];
CKShare *meForceGuidToExistMuhahaha = [[CKShare alloc] initWithRootRecord:testRecord_withGuid_private];
//meForceGuidToExistMuhahaha.publicPermission = CKShareParticipantPermissionNone;
CKModifyRecordsOperation *save = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:@[meForceGuidToExistMuhahaha,testRecord_withGuid_private]
recordIDsToDelete:nil];
save.savePolicy = CKRecordSaveAllKeys;
[save setModifyRecordsCompletionBlock: ^(
NSArray <CKRecord *> * _Nullable savedRecordArray,
NSArray <CKRecordID *> * _Nullable deletedRecordArray,
NSError * _Nullable modifyError
){
CKShare *savedShare;
CKRecord *savedRecord;
BOOL operationDidFail = NO;
if(modifyError != nil) {
NSArray *errorsToIgnore = @[@"record to insert already exists",@"Atomic failure"];
NSArray *partialErrors = modifyError.userInfo[@"CKPartialErrors"];
if(partialErrors == nil) {
operationDidFail = YES;
}
else {
for(id item in partialErrors) {
if([item isKindOfClass:[NSError class]]) {
NSError *partialError = item;
NSString *errorDesc = partialError.userInfo[@"ServerErrorDescription"];
if(errorDesc == nil) {
operationDidFail = YES;
break;
}
if(NO == [errorsToIgnore containsObject:errorDesc]) {
operationDidFail = YES;
break;
}
savedRecord = partialError.userInfo[@"ServerRecord"];
}
else {
LOG(@"<ERROR> Unexpected %@ encountered in CKPartialErrrors: %@",NSStringFromClass([item class]),item);
}
}
}
if(savedRecord == nil) {
operationDidFail = YES;
}
}
if(operationDidFail) {
LOG(@"<ERROR> Error saving testRecord_withGuid to private custom zone. Error: %@",modifyError);
return;
}
if(savedRecord != nil) {
// This could happen if the save policy is not CKRecordSaveAllKeys.
LOG(@"<NOTICE> Record already existed on server. Proceeding with fetch. Record: %@",savedRecord);
}
else {
LOG(@"savedRecords: %@",savedRecordArray);
savedShare = (CKShare *)savedRecordArray[0];
savedRecord = (CKShare *)savedRecordArray[1];
LOG(@"<NOTICE> Successfully upserted record to private custom zone with share URL: %@",[savedShare.URL absoluteString]);
}
/* Now that we successfully created a record with a short GUID to the private store, fetch it. */
[privateDB fetchRecordWithID:testRecordID_withGuid_private completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
if(error != nil) {
LOG(@"<ERROR> Error fetching testRecord_withGuid from private custom zone: %@",error);
return;
}
LOG(@"<NOTICE> Successfully fetched testRecord_withGuid from private custom zone: %@",record);
}];
}];
[save start];
}
@end
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With