Fetching CKRecordZone from a CKRecord - ios

Scenario:
I have a CKRecord which I have fetched from the server. The record exists inside a custom zone for which I do not know the identifier and do not have a CKRecordZone object for.
I need to make a call to CKDatabase.perform(query:inZoneWith:completion:) to get the records in the database which are components of the root shared record (which requires such a call) however without having a CKRecordZoneID (from a CKRecordZone) I am forced to iterate through every CKRecordZone in the shared database and perform the query until a matching record is found.
In summary: I want to take a CKRecord and find the CKRecordZone it exists in. Is this possible? Or is my method flawed and can I perform a query without the CKRecordZoneID?.

To find the CKRecordZoneID of a given record, the recordID property is helpful:
(record).recordID.zoneID yields the CKRecordZoneID that the CKRecord exists in.

Related

CoreStore how to fetch or query object from unspecified dynamic object

enter code hereI read this guide which documented pretty good. I need to search through my database and get a record actually execute fetch or query.
Can I search through all records instead of specifying appropriate From clause.
For example
var undefinedObject = CoreStore.fetchAll(
From(GoThroughAllMyDataBaseEntities),
Where("%K == %#", "localId", "some string id")
)
print(undefinedObject.id) // as object will be undefined I need to figure out how to get id property from it.
Side note: all my entities are child objects of parent entity which has id property.
So let's say I have next entities in CoreData:
BaseEntity (which includes id)
Playlist
Song
in the code above I don't care which will be returned to me I just what to see one that matches this condition: Where("%K == %#", "localId", "some string id")
Also my localId property in each objects are very unique strings. They are extracted from NSManagedObjectID.
So there is no way have the same duplicated identifier localy in CoreData
If there is no way to do it, then I will need to loop all my Playlists and Songs records.
managedObjectContext.objectWithID(objectID) may not work thought in some cases as there is no guaranty that CoreData record has not been deleted and app recreated a copy of the same record, so physically a copy of record has another objectID address in CoreData, but still has localID property copied from other record.

How to find out what root record is related to CKShare?

Simply I have a CKShare record. Having only this, I need to know what other root CKRecord is related with it.
Since there always need to be root record, I need to know it now. But can't see property in other place than in initializer.
public convenience init(rootRecord: CKRecord)
Any ideas?
You should use the CloudKit operation: CKFetchShareMetadataOperation. This operation is initialized with an array of CKShare().urls and returns the corresponding array of CKShareMetadata?. The CKShareMetadata class contains the rootRecordID: CKRecordID and other interesting properties like the associated share and the participantType: CKShareParticipantType. The participantType can be used to tell if the client who performed the fetch is the owner of the share.
It also contains the optional rootRecord: CKRecord?. You can force
fetch all root records by setting shouldFetchRootRecord to true on the CKFetchShareMetadataOperation operation instance before adding to a queue.

CloudKit: Preventing Duplicate Records

I am working through an app that pulls data from an external web service into a private CloudKit database. The app is a single user app, however I am running into a race condition that I am not sure how to avoid.
Every record in my external data has a unique identifier that I map to my CKRecord instances. The general app startup flow is:
Fetch current CKRecords for the relevant record type.
Fetch external records.
For every external record, if it doesn't exist in CloudKit, create it via batch create (modification operation).
Now, the issue is, if this process is kicked off on two of a user's devices simultaneously, since both the CK and external fetch is async, there is a strong possibility that I'll get duplicate records.
I know I can use zones to atomically commit all of my CKRecord instances, but I don't think that solves my issue because if all of these fetches happen at essential the same time, the save is not really the issue.
My questions are:
Does anyone know of a way to "lock" the private database for writes across all of a user's devices?
Alternatively, is there a way to enforce uniqueness on any CKRecord field?
Or, is there a way to use a custom value as the primary key, in that case I could use my external ID as the CK ID and allow the system to prevent duplicates itself.
Thanks for the help in advance!
Answers:
No, you cannot lock the private database
Cloudkit already enforces and assumes uniqueness of your record ID
You can make the record ID anything you like (in the non zone part of it).
Explanation:
Regarding your issue of duplication. If you are the one creating the record IDs (from the external records you mentioned for example) then at worst you should have one record over write the other with the same data if you have a race condition. I do not think that is an issue for the extreme case two devices kick off this process at the same time. Basically you logic of first fetching existing records and then modifying them seems sound to me.
Code:
//employeeID is a unique ID to identify an employee
let employeeID = "001"
//Remember the recordID needs to be unique within the same database.
//Assuming you have different record types, it is better to prefix the record name with the record type so that it is unique
let recordName = "Employee-\(employeeID)"
//If you are using a custom zone
let customZoneID = CKRecordZoneID(zoneName: "SomeCustomZone", ownerName: CKCurrentUserDefaultName)
let recordIDInCustomZone = CKRecordID(recordName: recordName, zoneID: customZoneID)
//If you are using the default zone
let recordIDInDefaultZone = CKRecordID(recordName: recordName)
I had similar issue of duplicates downloaded when I tried to read in a database of more than 100 records; the solution is found in the Apple's Atlas example which uses a Boolean to check if the last process finished before it launches the next. You find a block much like this...
#synchronized (self)
{
// Quickly returns if another loadNextBatch is running or we have the oldest post
if(self.isLoadingBatch || self.haveOldestPost) return;
else self.isLoadingBatch = YES;
}
Incidentally here the code to create your own record key.
CKRecordID *customID = [[CKRecordID alloc] initWithRecordName: [globalEOConfirmed returnEOKey:i]];
newrecord = [[CKRecord alloc] initWithRecordType:#"Blah" recordID:customID];

Any (or best) way to get records from CloudKit that aren't on the device?

I have the following predicate:
let predicate = NSPredicate(format: "NOT (recordID in %#)", recordIDs)
-- recordIDs is an array of CKRecordID objects corresponding to the CKRecords on the device
...that produces a runtime error about the predicate. If I change the predicate format string to something else, the query runs fine. I have the "Query" checkbox checked for all the metadata for this record type in CloudKit.
According to CKQuery documentation:
Key names used in predicates correspond to fields in the currently evaluated record. Key names may include the names of the record’s metadata properties such as "creationDate” or any data fields you added to the record.
According to CKRecord documentation, these are the available metadata for querying:
recordID, recordType, creationDate, creatorUserRecordID, modificationDate, lastModifiedUserRecordID, recordChangeTag
You can use the creation date:
NSPredicate(format: "creationDate > %#", dateLastFetched)
After you pull the records down to the device and save them, save the dateLastFetched and use it for subsequent fetches.
Edit: Be sure to enable the creationDate query index on the CloudKit dashboard (it is not enabled by default like many other indexes)
This is an old question, so I'm not sure if it existed at the time, but the correct way to do this now is to use Apple's built-in server change token support.
Making a giant query including all existing record ID's on the device is going to be slow, and picking a date is going to be imprecise.
The right way to do this is to use CKFetchRecordZoneChangesOperation and pass in a CKServerChangeToken using the operation's configurationsByRecordZoneID property.
The first time you call, pass a nil change token. As records are received, CloudKit will call recordZoneChangeTokensUpdatedBlock with the latest change token. Persist the latest token so the next time you need records from the server, you can just pass in your most recent token and get only what's changed since then.
Enable the meta data index by clicking here:

CloudKit - How to Save Record If Not Exists

I am trying to make a Record Type that contains unique values, and would act as the target reference objects to another Record Type. For example, Record Type - Movies would contain unique list of movies submitted by users. And FavoriteMovies would contain a Users reference and a Movies reference. Users could select from a list of existing Movies, or add new ones to it.
The problem happens if I create a new Movies record, while another user creates a new record with the same name (after I retrieved the list of movies, but before I attempt to add a new one). The two new records are considered different records with different recordIDs. This means that once I saved the new one, there will be two instances of Movies with the save name.
I'm not able to find a way to perform a Save If Not Exists type operation to the Movies Record Type. I could do a save in the completionBlock of a query, but those two actions would not be an atomic transaction to guarantee uniqueness. As far as I know this is also the case with chaining CKQueryOperation with CKModifyRecordsOperation.
Is there a way to insert a record only if the value does not exists in a single transaction?
If I understood correctly your use case, you can make movieRecord.recordID.recordName the movie's name and use CKModifyRecordsOperation with savePolicy IfServerRecordUnchanged to effectively Save If Not Exists. It would then return an error that you can ignore if you try to save a record that already exists on the server:
let saveRecordsOperation = CKModifyRecordsOperation()
saveRecordsOperation.recordsToSave = [movieRecord]
saveRecordsOperation.savePolicy = .IfServerRecordUnchanged
With the savePolicy IfServerRecordUnchanged this operation will save a new Movie record if it doesn't exist yet on the server (Save If Not Exists) but will return the error below on any subsequent try to overwrite a Movie record that already exists on the server (provided it is not a newer modified version of a record that was fetched from the server):
<CKError 0x14d23980: "Server Record Changed" (14/2017); server message = "record to insert already exists">
You could deal with this conflict in the perRecordCompletionBlock but in your specific use case you can just do nothing about the conflict error so each Movie record will be the first saved record with that CKRecordID.

Resources