CKModifyRecordsOperation - create referenced records simultaneously? - swift

I'm working to refactor a somewhat clunky iterative save loop for CloudKit to use CKModifyRecordsOperation and bulk save records.
I have a Course, which has 1+ Weeks, each which has 1+ Lessons. Previously I'd create the Course in CloudKit, then create the Weeks, then the Lessons and circle back to update the Weeks with the Lesson references once created. And also fetch and save the Course record with the references to the Weeks once Weeks were created.
I've refactored to create all (Course, Week and Lesson) records locally, with the relevant references set up. E.g., course["weeks"] contains the record references for each week I've created locally, for example:
course["weeks"] = getWeekRefsForCourse(for: allWeeks)
func getWeekRefsForCourse(for allWeeks: [CKRecord]) -> [CKRecord.Reference] {
var weekRefsArray: [CKRecord.Reference] = []
for each in allWeeks {
let weekRef = CKRecord.Reference(record: each, action: .deleteSelf)
weekRefsArray.append(weekRef)
return weekRefsArray
}
The issue is when I go to save, the error I get back is:
Invalid list of records: Cycle detected in record graph
This suggests that I've got a record referring to itself, but I've gone record by record and I I can't see anything. The Weeks reference the Course and the Lessons, but not themselves, etc. So my only theory is that because I'm trying to save items that refer to other items that haven't yet been created, what I'm trying to do isn't possible.
Is the correct protocol here actually my original approach? Or is there something I'm missing?
Original approach:
Save Course
Save Week
Save Lessons
Update Weeks with Lesson references
Update Course with Week references
CKModifyRecordsOperation code:
let bulkSaveQueryOp = CKModifyRecordsOperation()
bulkSaveQueryOp.recordsToSave = [courseRecord]
bulkSaveQueryOp.recordsToSave?.append(contentsOf: weeks)
bulkSaveQueryOp.recordsToSave?.append(contentsOf: lessons)
//note I've confirmed I have the correct number of records
bulkSaveQueryOp.modifyRecordsCompletionBlock = { records, recordIDs, error in
if let error = error as? CKError {
log.error(error)
} else { // success }
}
CKContainer.default().publicCloudDatabase.add(bulkSaveQueryOp)

I'm fairly certain you can create all these records together. I suspect there must be something else wrong and that's why you are getting that error.
You can create a CKReference to an object that doesn't even exist and CloudKit will still create it. A CKReference is little more than a pointer to a recordName of another object in the container.
Combining all those records into a CKModifyRecordsOperation is the right thing to do and you shouldn't have to be careful about the order of your CKRecord saves. I think another issue must be lurking somewhere.

I was able to find a similar question over in the apple dev forums for someone that had a similar issue and and it helped identify the source of the problem - that I had effectively created a loop with my .deleteSelf actions I was creating on various references.
So while the references were fine, the actions were what was causing the error.
Once I double checked and adjusted those, the error went away and I was able to save.
Spotting this was made more complex by the fact that I wasn't changing the .deleteSelf actions version what I had previously done - and was working fine - when my saves were done in sequence rather than a bulk CKModifyRecordsOperation save.
So it seams that another added benefit of CKModifyRecordsOperation with a bulk save is that it adds a layer of stupidity checking that creating items individually doesn't :)

Related

How do I limit the number of times a user can perform an action in Swift using CloudKit?

I'm trying to limit the number of times a user can perform an action in a month. I understand that storing a counter in NSUserDefaults is low security as the user can easily go in and modify the stored variable, which I want to avoid. Since I don't have a server, I was thinking of using CloudKit to store a user's possible allocated number of times along with their current count - is that even possible?
1- I've gone ahead and created a 'Record Type' called UserLimits with two int keys: 'call_current' and 'call_limit' in the public database. Do I also need to store the user's UserRecordID as well?
2- For permissions, I've opted to set UserLimits as Read for _world, _creator, _icloud so that no user can modify these variables.
At this point I'm kind of stuck as I can't seem to find any documentation outlining similar steps, or whether I'm doing things correctly or not.
Fetching a user's ID is as easy as:
CKContainer.default().fetchUserRecordID { recordID, error in
guard let recordID = recordID, error == nil else {
// error handling magic
return
}
print("Got user record ID \(recordID.recordName).")
}
How would I use this ID to fetch their current limit, update their count, etc?
Any help would be greatly appreciated!

How to set more than one value to a child in Firebase using Swift?

I am trying to make a money related app which shows the user their spendings as a project to get used to Firebase. So I stumbled upon this issue; I can't seem to figure out how to add more than one expense assigned to a user. Whenever I add a new expense, the existing value in Firebase gets reset to the new value but I want it to store both the new and the old value. How can I do this?
There's something called "autoID" if that helps. I'll link that in a few moments.
Edit: It's used here.
childByAutoId() is what you want for swift. See Updating Or Deleting specific data section in the Read and Write Data on iOS
let thisUserRef = ref.child("users").child(users uid)
let expenseRef = thisUserRef.child("expenses").childByAutoId()
let dict = ["expense_type": "travel", "expense_amt": "1.99"]
expenseRef.setValue(dict)
will results in
users
uid_0
-Y88jn90skda //<- the node key created with childByAutoId
expense_type: "travel"
expense_amt: "1.99"
the childByAutoId is super powerful and allows 'random' node keys to be generated that contain your child data.
See the answer to this question and this other question for some tips on data modeling and queries.

How to save one value of Parse object without overwriting entire object?

I have two users accessing the same object. If userA saves without first fetching the object to refresh their version, data that userB has already successfully saved would be overwritten. Is there any way(perhaps cloud code?) to access and update one, and only one, data value of a PFObject?
I was thinking about pushing the save out to the cloud, refreshing the object once it gets there, updating the value in the cloud, and then saving it back. However that's a pain and still not without it's faults.
This seems easy enough, but to me was more difficult than it should have been. Intuitively, you should be able to filter out the fields you don't want in beforeSave. Indeed, this was the advice given in several posts on Parse.com. In my experience though, it would actually treat the filtering as deletions.
My goal was a bit different - I was trying to filter out a few fields and not only save a few fields, but translating to your context, you could try querying the existing matching record, and override the new object. You can't abort via response.failure(), and I don't know what would happen if you immediately save the existing record with the field of interest and null out the request.object property - you could experiment on your own with that:
Parse.Cloud.beforeSave("Foo", function(request, response) {
// check for master key if client is not end user etc (and option you may not need)
if (!request.master) {
var query = new Parse.Query("Foo");
query.get(request.object.id).then(function(existing) {
exiting.set("some_field", request.object.get("some_field"));
request.object = exiting; // haven't tried this, otherwise, set all fields from existing to new
response.success();
}, function(error) {
response.success();
});
}
});

Swift: Core Data - Resolving a logic issue with object creation

So, my Swift app allows a user to choose sports teams to see historic match information for. Currently, a user selects team(s) and the JSON data file of historic matches is scanned.
If a historic match includes a name of a selected team, the details of the match are stored in a Core Data entity, which is fed into my main Table View.
However, this presents an issue I can't get my head around solving.
If a user selects team A and B, and the database contains a match where team A and B played EACH OTHER, two objects for the match details are created, and as such, Table View cell is created twice, once for team A being found in the instance of the match, and again for team B.
Is there an easy and efficient way to trim any duplicates caused in this way? I don't know whether to handle this at the object creation time, or just to find a way of removing any duplicated cells from my Table View.
Thanks so much.
I think you should redesign your setup. Have all the records to be searched stored in Core Data.
If you have a hardcoded JSON file - import it on first start. If you have retrieved JSON - insert / update the elements that are new / changed in your Core Data object graph.
You would have a Match or Game entity and it would be retrieved only once. The fetch predicate would be something like
NSPredicate(format: "homeTeam = %# || guestTeam = %#", selectedTeam, selectedTeam)

Salesforce- Trigger - Deleting object

Hello fellow code lovers, I am attempting to learn apex code and stuck on a problem. Since most of you guys here are avid code lovers and excited about problem solving, I figured I could ran by problem by.
I am attempting to Create a Trigger on an object called Book that does the following:
On Delete, all associated Chapters are also deleted
There is also an object named Chapter that is has a lookup to book.
Here is my attempt. This is my first ever attempt at apex so please be patient. Is anyone willing to dabble in this piece of code?
trigger DeleteChaptersTrigger on Book__c (before delete) {
List<Book__c> book = Trigger.old;
List<Chapter__c> chapters = new List<Chapter__c>();
Set set = new Set();
for (Book__c b :books){
if ()
}
}
You need to write all trigger with consideration that the trigger might be processing many records at any one time so you need to bulkify your trigger code.
Here are the variables that available on the trigger object.
You want to get all the record ids that will be deleted. Use the keyset method on the oldmap to get this info without looping and creating your own collection. Then you can just delete the records returned from the query.
trigger DeleteChaptersTrigger on Book__c (before delete) {
Set<string> bookids = Trigger.oldMap.keyset();
delete [SELECT Id FROM Chapter__c WHERE Book__c IN :bookids];
}
Apex code is unlike other languages where it gets confused with reuse of used words, like set as a variable name. Apex is also case insensitive.
Since you have full control, I recommend changing the way your custom objects are related to each other.
A chapter has no meaning/value without a book so we want to change the relationship between the two.
Remove the lookup on the Chapter object and replace it with a master-detail. When a master record gets deleted, Salesforce automatically deletes the detail related records. This is what you want, and without coding.