Error while trying to save two records with CKReference to each other

I'm getting the following error...

Invalid list of records: Cycle detected in record graph

This happens when I try to save two records with references to each other. The two record types I have are called Game and Chat. A Chat is part of a game (eventually I will have Message(s) as part of Chat which would be a one to many relationship but will tackled that another time.)

I create two CKReference objects. One for Game and one for my Chat object. I then put this into a CKModifyRecordsOperation and then get the error above. I'm uncertain what I'm missing?

Here is how I'm creating the records...

Code Block
CKRecordZoneID *zoneID = [[CKRecordZone alloc] initWithZoneName:GameZoneName].zoneID;
self.record = [[CKRecord alloc] initWithRecordType:@"Game" zoneID:zoneID];
[self setCoreValues:self fromRecord:self.record];
self.record[@"name"] = self.name = theGame.name;
self.chat = [[CKRecord alloc] initWithRecordType:@"Chat" zoneID:zoneID];
self.chat[@"name"] = theGame.chat.name;
CKReference *gameRef = [[CKReference alloc] initWithRecordID:self.record.recordID action:CKReferenceActionNone];
self.chat[@"game"] = gameRef;
[self.chat setParent:gameRef];
CKReference *chatRef = [[CKReference alloc] initWithRecordID:self.chat.recordID action:CKReferenceActionDeleteSelf];
self.record[@"chat"] = chatRef;


I then use the following to save the record to CloudKit...

Code Block
CKContainer *container = [CKContainer defaultContainer];
CKDatabase *db = container.privateCloudDatabase;
NSLog (@"Saving Game with name = %@", self.record[@"name"]);
NSMutableArray *records = [[NSMutableArray alloc] init];
[records addObject:self.record];
[records addObject:self.chat];
NSLog(@"self.record.recordID = %@", self.record.recordID);
NSLog(@"self.record[chat] = %@", self.record[@"chat"]);
NSLog(@"self.chat.recordID = %@", self.chat.recordID);
NSLog(@"self.chat[game] = %@", self.chat[@"game"]);
CKModifyRecordsOperation *operation = [[CKModifyRecordsOperation alloc] initWithRecordsToSave:records recordIDsToDelete:nil];
operation.atomic = NO;
operation.database = db;
[operation setModifyRecordsCompletionBlock:^(NSArray<CKRecord *> * _Nullable savedRecords, NSArray<CKRecordID *> * _Nullable deletedRecordIDs, NSError * _Nullable operationError) {
if (operationError)
{
failure (operationError);
}
else
{
success (savedRecords);
}
}];
if (self.saveQueue == nil)
self.saveQueue = [[NSOperationQueue alloc] init];
[self.saveQueue addOperation:operation];


My operation hits line 24 and outputs the error above. The only thing I can think of is that I'm creating the references incorrectly? But everything I've seen both in the documentation and online resources shows this as being correct.
Ok I've narrowed the issue down. If my call to setParent on the Chat object (line 13 above of the first code listing). If I remove this line. Everything works and my two references are saved so each record has a reference to each other.

However I believe then that I'm missing the benefits of using setParent? So that when I do an update to a child object things are kept in sync? Or is setParent only to be used when using CoreData/CloudKit syncing that was introduced in 2019 with NSPersistentCloudKitContainer? I'm not using that (as much as I'd like to) because I need to share my records. So I'm doing the Core Data/Cloud Kit syncing myself.
Ok. I think I resolved it...

What it came down to is the referenceAction. This was set backwards. This code from my first code snippet above...

Code Block
CKReference *gameRef = [[CKReference alloc] initWithRecordID:self.record.recordID action:CKReferenceActionNone]; self.chat[@"game"] = gameRef;
[self.chat setParent:gameRef];

Has the action on the CKReference set to CKReferenceActionNone. This was because when I had this set, correctly, to CKReferenceActionDeleteSelf it would cause a crash with the exception : Parent references must be CKReferenceActionNone with userInfo = (null)

But what I realized is that I had my Chat reference, stored in my Game object set to deleteSelf.
Code Block
CKReference *chatRef = [[CKReference alloc] initWithRecordID:self.chat.recordID action:CKReferenceActionDeleteSelf]; self.record[@"chat"] = chatRef;

self.record = Game record. Just to clarify.

So in this setup if a chat record was deleted, then the whole game would be deleted. Which was counter to having the Game be the PARENT of the Chat record. Which is why the exception Invalid list of records: Cycle detected in record graph was thrown.

The solution? setParent seems to be a special case. Sending it a CKReference with deleteSelf set is invalid for some reason. Not sure why causes this second error. Perhaps someone can explain that? But the solution is to use ANOTHER CKReference. Or specifically, a different function. I now use setParentReferenceFromRecord on my Chat record.

Code Block
[self.chat setParentReferenceFromRecord:self.record];

This then correctly sets the parent with a new reference and I have my existing reference. It seems a bit double duty here. I have two fields (a system and custom) with the same data except for the "ReferenceAction". So I believe that if I delete the Game record, my custom field's CKReference with deleteSelf set on the Chat record will cause the cascade. The system record Parent doesn't really apply here. So now I'm left with, why use it???

For clarity for anyone else who comes across this, here is the full code listing of creating my two records again... The code that saves them is unchanged.

Code Block
CKRecordZoneID *zoneID = [[CKRecordZone alloc] initWithZoneName:GameZoneName].zoneID;
self.record = [[CKRecord alloc] initWithRecordType:@"Game" zoneID:zoneID];
[self setCoreValues:self fromRecord:self.record];
self.record[@"name"] = self.name = theGame.name;
// Make Chat
self.chat = [[CKRecord alloc] initWithRecordType:@"Chat" zoneID:zoneID];
self.chat[@"name"] = theGame.chat.name;
CKReference *gameRef = [[CKReference alloc] initWithRecordID:self.record.recordID action:CKReferenceActionDeleteSelf];
self.chat[@"game"] = gameRef;
[self.chat setParentReferenceFromRecord:self.record];
CKReference *gameChatRef = [[CKReference alloc] initWithRecordID:self.chat.recordID action:CKReferenceActionNone];
self.record[@"chat"] = gameChatRef;

Error while trying to save two records with CKReference to each other
 
 
Q