CKFetchDatabaseChangesOperation returns no record zone IDs - cloudkit

I'm using CloudKit in my app and have begun by following the best practises in the WWDC video"CloudKit Best Practises"
The first thing to do is to check for changes which I do like so,
let changesOperation = CKFetchDatabaseChangesOperation(previousServerChangeToken: databaseChangeToken)
changesOperation.fetchAllChanges = true
changesOperation.recordZoneWithIDChangedBlock = { self.recordZoneWithIDChanged($0) }
changesOperation.recordZoneWithIDWasDeletedBlock = { self.recordZoneWithIDWasDeleted($0) }
changesOperation.changeTokenUpdatedBlock = { self.changeTokenUpdate($0) }
changesOperation.fetchDatabaseChangesCompletionBlock = { self.fetchDatabaseChangesCompletion($0, isMoreComing: $1, error: $2) }
privateDatabase.add(changesOperation)
There are records in the private database that I am setting up the fetch for, but I only ever get the changeTokenUpdatedBlock and the fetchDatabaseChangesCompletion.
Am I right in saying I should expect to see recordZoneWithIDChangedBlock being hit when I run this operation and my private database's default zone to be passed in to this block?
It means when I call my fetchDatabaseChangesCompletion, there's nothing to fetch because the array of record zone IDs is empty: (note, error is nil)
fileprivate func fetchDatabaseChangesCompletion(_ newToken: CKServerChangeToken?, isMoreComing: Bool, error: Error?)
{
if let error = error
{
// Handle error
return
}
let fetchZoneChangesOperation = CKFetchRecordZoneChangesOperation(recordZoneIDs: changedRecordZoneIDs,
optionsByRecordZoneID: nil)
fetchZoneChangesOperation.recordChangedBlock = { self.recordChanged($0) }
fetchZoneChangesOperation.recordWithIDWasDeletedBlock = { self.recordWithIDWasDeleted($0, string: $1) }
fetchZoneChangesOperation.recordZoneFetchCompletionBlock = { self.recordZoneFetchCompletion($0, newChangeToken: $1, clientSentChangeTokenData: $2, isMoreComing: $3, error: $4) }
fetchZoneChangesOperation.completionBlock = { self.fetchRecordZoneChangesCompletion() }
privateDatabase.add(fetchZoneChangesOperation)
}

I ran into this same problem and it is due to CKFetchDatabaseChangesOperation and CKFetchRecordZoneChangesOperation only working on custom zones. CloudKit really wants developers to compartmentalize data so they support more capabilities in custom zones.
The disadvantage of using the default zone for storing records is that it does not have any special capabilities. You cannot save a group of records to iCloud atomically in the default zone. Similarly, you cannot use a CKFetchRecordChangesOperation object on records in the default zone.
CKRecordZone default() Reference
CKFetchRecordChangesOperation was deprecated in iOS 10 and replaced with CKFetchRecordZoneChangesOperation.

Related

Ambiguous reference to member 'save(_:completionHandler:)' with CloudKit save attempt

I'm trying to save back to CloudKit after updating a reference list and getting the error on the first line of this code block.
Error: Ambiguous reference to member 'save(_:completionHandler:)'
CKContainer.default().publicCloudDatabase.save(establishment) { [unowned self] record, error in
DispatchQueue.main.async {
if let error = error {
print("error handling to come")
} else {
print("success")
}
}
}
This sits within a function where the user going to follow a given location (Establishment). We're taking the existing establishment, and its record of followers, checking to see if the selected user is in it, and appending them to the list if not (or creating it if the list of followers is null).
Edit, in case helpful
//Both of these are passed in from the prior view controller
var establishment: Establishment?
var loggedInUserID: String?
#objc func addTapped() {
// in here, we want to take the logged in user's ID and append it to the list of people that want to follow this establishment
// which is a CK Record Reference
let userID = CKRecord.ID(recordName: loggedInUserID!)
var establishmentTemp: Establishment? = establishment
var followers: [CKRecord.Reference]? = establishmentTemp?.followers
let reference = CKRecord.Reference(recordID: userID, action: CKRecord_Reference_Action.none)
if followers != nil {
if !followers!.contains(reference) {
establishmentTemp?.followers?.append(reference)
}
} else {
followers = [reference]
establishmentTemp?.followers = followers
establishment = establishmentTemp
}
[this is where the CKContainer.default.....save block pasted at the top of the question comes in]
I've looked through the various posts on 'ambiguous reference' but haven't been able to figure out the source of my issue. tried to explicitly set the types for establisthmentTemp and followers in case that was the issue (based on the solutions to other related posts) but no luck.
Afraid I'm out of ideas as a relatively inexperienced newbie!
Help appreciated.
Documenting the solution that I figured out:
Combination of two issues:
I was trying to save an updated version of a CK Record instead of updating
I was not passing a CK Record to the save() call - but a custom object
(I believe point two was the cause of the 'ambiguous reference to member'
error)
I solved it by replacing the save attempt (first block of code in the question) with:
//first get the record ID for the current establishment that is to be updated
let establishmentRecordID = establishment?.id
//then fetch the item from CK
CKContainer.default().publicCloudDatabase.fetch(withRecordID: establishmentRecordID!) { updatedRecord, error in
if let error = error {
print("error handling to come")
} else {
//then update the 'people' array with the revised one
updatedRecord!.setObject(followers as __CKRecordObjCValue?, forKey: "people")
//then save it
CKContainer.default().publicCloudDatabase.save(updatedRecord!) { savedRecord, error in
}
}
}

Trouble finding out if this counts as a read/many reads/will I get charged loads on database costs?

I am currently developing an iOS app with a google cloud firestore as a backend and I am using a few listeners to find out if data is updated and then pushing it to my device accordingly. I wrote this function that listens for a value if true or not and according to so will update an animation in my app. The trouble is I don't know if I wrote it properly and don't want to incur unnecessary reads from my database if I don't have to.
func dingAnimation() {
let identifier = tempDic![kBOUNDIDENTIFIER] as! String
if identifier != "" {
dingListener = reference(.attention).document(identifier).addSnapshotListener({ (snapshot, error) in
if error != nil {
SVProgressHUD.showError(withStatus: error!.localizedDescription)
return
}
guard let snapshot = snapshot else { return }
let data = snapshot.data() as! NSDictionary
for dat in data {
let currentId = FUser.currentId() as! String
let string = dat.key as! String
if string == currentId {
} else {
let value = dat.value as! Bool
self.shouldAnimate = value
self.animateImage()
}
}
})
}
}
This might help you.
From Firestore DOCS - Understand Cloud Firestore billing
https://firebase.google.com/docs/firestore/pricing
Listening to query results
Cloud Firestore allows you to listen to the results of a query and get realtime updates when the query results change.
When you listen to the results of a query, you are charged for a read each time a document in the result set is added or updated. You are also charged for a read when a document is removed from the result set because the document has changed. (In contrast, when a document is deleted, you are not charged for a read.)
Also, if the listener is disconnected for more than 30 minutes (for example, if the user goes offline), you will be charged for reads as if you had issued a brand-new query.

CloudKit - recordZoneFetchCompletionBlock shows 1000's of deleted records

I noticed something strange during testing. First, I Erase All Content and Settings on the simulator, and then manually delete all records in CloudKit. When I first run the app, I've noticed that over 2000 records are being deleted. I don't understand why (or even where!) they are being stored. Have I completely missed something? Below is a portion of the CloudKit method that is run as part of a check for updates.
operation.fetchDatabaseChangesCompletionBlock = { (token, more, error) in
if error != nil {
finishClosure(UIBackgroundFetchResult.failed)
} else if !zonesIDs.isEmpty {
changeToken = token
let configuration = CKFetchRecordZoneChangesOperation.ZoneConfiguration()
configuration.previousServerChangeToken = changeZoneToken
let fetchOperation = CKFetchRecordZoneChangesOperation(recordZoneIDs: zonesIDs, configurationsByRecordZoneID: [zonesIDs[0]: configuration])
fetchOperation.recordChangedBlock = { (record) in
listRecordsUpdated.append(record)
}
fetchOperation.recordWithIDWasDeletedBlock = { (recordID, recordType) in
if changeToken != nil {
listRecordsDeleted[recordID.recordName] = recordType
}
}
fetchOperation.recordZoneChangeTokensUpdatedBlock = { (zoneID, token, data) in
changeZoneToken = token
}
fetchOperation.recordZoneFetchCompletionBlock = { (zoneID, token, data, more, error) in
if error != nil {
print("Error")
} else {
changeZoneToken = token
self.updateLocalRecords(listRecordsUpdated: listRecordsUpdated)
self.deleteLocalRecords(listRecordsDeleted: listRecordsDeleted)
listRecordsUpdated.removeAll()
listRecordsDeleted.removeAll()
}
}
etc.
Delete Records
func deleteLocalRecords(listRecordsDeleted: [String : String]) {
for (recordName, recordType) in listRecordsDeleted {
let request: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "\(recordType)")
request.predicate = NSPredicate(format: "ckrecordname = %#", recordName)
do {
let result = try context.fetch(request)
if !result.isEmpty {
if let data = result[0] as? NSManagedObject {
context.delete(data)
}
}
}
catch {
print("Error fetching")
}
}
coreData.saveContext()
}
It sounds like you're deleting the records through the dashboard but are keeping the record zone. In that case the deletes are part of the history of the zone, and when you first sync with the zone it basically rewinds through all that history for the zone, which at the end includes deletes for all your records.
Keep in mind this also applies to zones - so a zone delete for example stays in the history and can lead to some unwanted situations if you don't account for that. I ran into the situation where I was deleting the zone on one device, but the other one would then try to sync, find no zone and create it again.

Firebase audience based on user properties

I need to test some features of my app with just a previously selected group of users.
I created an Audience where user_id exactly matches 123456. 123456 being my own ID.
In Remote Config I created a Condition that matches users in the Audience above.
Then I created a parameter in Remote Config called feature_available and for the condition, I set a return value of true. The Default value is false.
In my app I set up Firebase:
FIRApp.configure()
let remoteConfig = FIRRemoteConfig.remoteConfig()
if let remoteConfigSettings = FIRRemoteConfigSettings(developerModeEnabled: false) {
remoteConfig.configSettings = remoteConfigSettings
}
remoteConfig.setDefaultsFromPlistFileName("FirebaseRemoteConfigDefaults")
And set the user ID:
FIRAnalytics.setUserID("123456")
Then I fetch from Firebase:
var expirationDuration: Double
// If in developer mode cacheExpiration is set to 0 so each fetch will retrieve values from the server.
expirationDuration = remoteConfig.configSettings.isDeveloperModeEnabled ? 0 : 3600
remoteConfig.fetch(withExpirationDuration: TimeInterval(expirationDuration)) { (status, error) -> Void in
if status == .success {
remoteConfig.activateFetched()
} else {
assertionFailure("Firebase config not fetched. Error \(error!.localizedDescription)")
}
}
The last thing I do is to get the value from Firebase and check if I have the feature enable:
let featureIsAvailable = remoteConfig["feature_available"].boolValue
if featureIsAvailable { ... }
The problem is that every single time the value returns from Firebase it is false and I can't manage to get it to return the correct value that matches that Audience I created.
I also tried to do it setting a user property instead of using setUserID() and had the same result.
Any suggestions?
I've run into similar issues before, sometimes it can take a while for the fetch to finish. The check if the feature is available needs to be done when the config is successfully fixed. Something like this hopefully works for you as well:
var featureIsAvailable : Bool?
override func viewDidLoad() {
super.viewDidLoad()
configureRemoteConfig()
fetchConfig()
}
func configureRemoteConfig() {
remoteConfig = FIRRemoteConfig.remoteConfig()
// Create Remote Config Setting to enable developer mode.
// Fetching configs from the server is normally limited to 5 requests per hour.
// Enabling developer mode allows many more requests to be made per hour, so developers
// can test different config values during development.
let remoteConfigSettings = FIRRemoteConfigSettings(developerModeEnabled: true)
remoteConfig.configSettings = remoteConfigSettings!
remoteConfig.setDefaultsFromPlistFileName("RemoteConfigDefaults")
}
func fetchConfig() {
var expirationDuration: Double = 3600
// If in developer mode cacheExpiration is set to 0 so each fetch will retrieve values from
// the server.
if (self.remoteConfig.configSettings.isDeveloperModeEnabled) {
expirationDuration = 0
}
// cacheExpirationSeconds is set to cacheExpiration here, indicating that any previously
// fetched and cached config would be considered expired because it would have been fetched
// more than cacheExpiration seconds ago. Thus the next fetch would go to the server unless
// throttling is in progress. The default expiration duration is 43200 (12 hours).
remoteConfig.fetch(withExpirationDuration: expirationDuration) { (status, error) in
if (status == .success) {
print("Config fetched!")
self.remoteConfig.activateFetched()
let featureIsAvailable = self.remoteConfig["feature_available"]
if (featureIsAvailable.source != .static) {
self.featureIsAvailable = featureIsAvailable.boolValue
print("should the feature be available?", featureIsAvailable!)
}
} else {
print("Config not fetched")
print("Error \(error)")
}
self.checkIfFeatureIsAvailable()
}
}
func checkIfFeatureIsAvailable() {
if featureIsAvailable == false {
// Don't show new feature
} else {
// Show new feature
}
}
I sent an email to the Firebase team requesting support and they told me that the issue was a bug in Xcode 8(.0 and .1) using Swift.
Updating to the latest version released today, 8.2, fixed the issue.

Can't retrieve CKRecords to resolve conflicts on CKErrorCodeServerRecordChanged in CKModifyRecordsOperation

I ran into some problem when trying to handle errors when doing batch operations on records in CloudKit.
I am successfully extracting the dictionary containing the partial errors, which I can iterate over. However, I am not able to get the records needed to resolve the conflict for CKErrorCodeServerRecordChanged. According to the docs I should be able to get 3 records out of the dictionary:
CKRecordChangedErrorServerRecordKey
CKRecordChangedErrorAncestorRecordKey
CKRecordChangedErrorClientRecordKey
Thank you for any hints on what I am doing wrong here.
func pushRecordChangesForZoneID(recordZoneID: CKRecordZoneID) {
// ...
modifyRecordsOperation.modifyRecordsCompletionBlock = { (savedRecords, deletedRecordIDs, error) -> Void in
if (error != nil) {
if error.code == CKErrorCode.PartialFailure.rawValue {
if let errorDict = error.userInfo?[CKPartialErrorsByItemIDKey] as? [CKRecordID : NSError] {
for (recordID, partialError) in errorDict {
if partialError.code == CKErrorCode.ServerRecordChanged.rawValue {
if let userInfo = partialError.userInfo {
let serverRecord = userInfo[CKRecordChangedErrorServerRecordKey] as? CKRecord
// serverRecord will always be nil
}
}
}
}
}
}
}
}
Additional information: When I print the description of the userInfo dict of the partial error (partialError.userInfo) it doesn’t look like it contains the other CKRecords:
[NSDebugDescription: CKInternalErrorDomain: 2037, NSLocalizedDescription: Error saving record <CKRecordID: 0x7fb41bf7e640; DA39FE08-AB0B-4F07-A42E-F5732B114706:(userData:__defaultOwner__)> to server: Protection data didn't match, NSUnderlyingError: <CKError 0x7fd89a92d370: "Unknown Error" (2037)>]
The description of the source error's dictionary (errorDict) looks like this (and I can successfully get the dictionary containing the recordIDs and partial errors out via CKPartialErrorsByItemIDKey):
[<CKRecordID: 0x7fb5bb88afa0; C1575083-F992-448A-8D77-D62C4A42D696:(userData:__defaultOwner__)>: <CKError 0x7fb5b961c6a0: "Batch Request Failed" (22/2024); server message = "Atomic failure"; uuid = 1E4C0FD5-EC10-4071-B277-102A9F1B0E5E; container ID = "iCloud.net.neverthesamecolor.atsumeru">, <CKRecordID: 0x7fb5bb848ad0; DA39FE08-AB0B-4F07-A42E-F5732B114706:(userData:__defaultOwner__)>: <CKError 0x7fb5b9653060: "Server Record Changed" (14/2037); "Error saving record <CKRecordID: 0x7fb41bd9ca50; DA39FE08-AB0B-4F07-A42E-F5732B114706:(userData:__defaultOwner__)> to server: Protection data didn't match">]
The docs say it's a custom zone feature only.
Edit 21st Jan 2016: It's working for me right now even in the default zone and public database. This is a big change from before.