CloudKit - recordZoneFetchCompletionBlock shows 1000's of deleted records - sql-delete

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.

Related

I cannot get the AWS Cognito credentials of a user (swiftUI)

I have tried a couple of different things, and at this point I am stumped. I simply want to be able to access the user's email to present it in a view. However I have not been able to successfully present, much less retrieve, this information. Here are the two pieces of code I have tried with:
func getUsername() -> String? {
if(self.isAuth) {
return AWSMobileClient.default().username
} else {
return nil
}
}
and
func getUserEmail() -> String {
var returnValue = String()
AWSMobileClient.default().getUserAttributes { (attributes, error) in
if(error != nil){
print("ERROR: \(String(describing: error))")
}else{
if let attributesDict = attributes{
//print(attributesDict["email"])
self.name = attributesDict["name"]!
returnValue = attributesDict["name"]!
}
}
}
print("return value: \(returnValue)")
return returnValue
}
Does anyone know why this is not working?
After sign in try this:
AWSMobileClient.default().getTokens { (tokens, error) in
if let error = error {
print("error \(error)")
} else if let tokens = tokens {
let claims = tokens.idToken?.claims
print("claims \(claims)")
print("email? \(claims?["email"] as? String ?? "No email")")
}
}
I've tried getting the user attributes using AWSMobileClient getUserAttributes with no success. Also tried using AWSCognitoIdentityPool getDetails With no success. Might be an error from AWS Mobile Client, but we can still get attributes from the id token, as seen above.
If you are using Hosted UI, remember to give your hosted UI the correct scopes, for example:
let hostedUIOptions = HostedUIOptions(scopes: ["openid", "email", "profile"], identityProvider: "Google")
It is because it is an async function so will return but later than when the function actually ends with the value. Only way I found to do it is placing a while loop and then using an if condition.

Error trying to save CKServerChangeToken to UserDefaults

Following the answer here:
Save CKServerChangeToken to Core Data
The code fails when I try to save the new token to user defaults.
I get the error:
Could not cast value of type 'Foundation.__NSSwiftData' (0x1dad7a900) to 'CKServerChangeToken' (0x1da7fae08).
2019-07-28 12:05:41.594726-0700 HintApp[20235:1921952] Could not cast value of type 'Foundation.__NSSwiftData' (0x1dad7a900) to 'CKServerChangeToken' (0x1da7fae08).
just saving the CKServerChangeToken as an Any to user defaults.
public extension UserDefaults {
var serverChangeToken: CKServerChangeToken? {
get {
guard let data = self.value(forKey: changeTokenKey) as? Data else {
return nil
}
let token: CKServerChangeToken?
do {
token = try NSKeyedUnarchiver.unarchivedObject(ofClass: CKServerChangeToken.self, from: data)
} catch {
token = nil
}
return token
}
set {
if let token = newValue {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true)
self.set(data, forKey: changeTokenKey)
} catch {
// handle error
print("error setting change token:\(error)")
}
} else {
self.removeObject(forKey: changeTokenKey)
}
}
}
}
and then
func fetchDatabaseChanges(database: CKDatabase, databaseTokenKey: String, completion: #escaping () -> Void) {
var changedZoneIDs: [CKRecordZone.ID] = []
let changeToken = UserDefaults.standard.serverChangeToken
let operation = CKFetchDatabaseChangesOperation(previousServerChangeToken: changeToken)
operation.recordZoneWithIDChangedBlock = { (zoneID) in
changedZoneIDs.append(zoneID)
}
operation.recordZoneWithIDWasDeletedBlock = { (zoneID) in
// Write this zone deletion to memory
}
operation.changeTokenUpdatedBlock = { (token) in
// Flush zone deletions for this database to disk
// Write this new database change token to memory
print("saving new token \(token)")
UserDefaults.standard.serverChangeToken = token
}
operation.fetchDatabaseChangesCompletionBlock = { (token, moreComing, error) in
if let error = error {
print("Error during fetch shared database changes operation", error)
completion()
return
}
// Flush zone deletions for this database to disk
// Write this new database change token to memory
self.fetchZoneChanges(database: database, databaseTokenKey: databaseTokenKey, zoneIDs: changedZoneIDs) {
// Flush in-memory database change token to disk
completion()
}
}
operation.qualityOfService = .userInitiated
database.add(operation)
}
outcome:
saving new token <CKServerChangeToken: 0x2834ecf90; data=REDACTED==>
Could not cast value of type 'Foundation.__NSSwiftData' (0x1dad7a900) to 'CKServerChangeToken' (0x1da7fae08).
2019-07-28 12:05:41.594726-0700 HintApp[20235:1921952] Could not cast value of type 'Foundation.__NSSwiftData' (0x1dad7a900) to 'CKServerChangeToken' (0x1da7fae08).
**REDACTED contains actual data I don't want to share on here. Unrelated to my error.
I'm sorry. I'm an idiot. A bit further down I was trying to pull CKServerChangeToken right out of userDefaults directly. I changed it to use the extension and it made it past that and is now cheerfully presenting me with error fetching zone change messages, which should go in a new question.

How to Queue Up Lots of Inbound CloudKit Notifications

Let's say I save 50 records to CloudKit with a CKModifyRecordsOperation like this:
let operation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
operation.savePolicy = .changedKeys
operation.modifyRecordsCompletionBlock = { records, _, error in
//...
}
privateDB.add(operation)
On my other devices, I get sprayed with 50 different background notifications for each CKRecord that changed. That's fine, I expect that.
I process each inbound notification like this:
func processCloudKitNotification(notification: CKNotification, database: CKDatabase){
guard let notification = notification as? CKQueryNotification else { return }
if let recordID = notification.recordID{
var recordIDs = [CKRecordID]()
switch notification.queryNotificationReason {
case .recordDeleted:
//Delete [-]
//...
default:
//Add and Edit [+ /]
recordIDs.append(recordID)
}
let fetchOperation = CKFetchRecordsOperation(recordIDs: recordIDs)
fetchOperation.fetchRecordsCompletionBlock = { records, error in
//...
}
database.add(fetchOperation)
}
}
But since each of the 50 incoming notifications are separate events, this function gets called 50 different times, thus triggering a slew of requests to pull down the full records using the CKRecordIDs that the notifications give me.
How can I queue up all the incoming notification CKRecordIDs within a reasonable period of time so that I can make a more efficient CKFetchRecordsOperation request with more than one recordID at a time?
I ended up using a timer for this, and it works quite well. Basically I start a timer when a new push notification comes in, and I reset it each time an additional notification arrives. Meanwhile, I collect all the CKRecordID's that are coming in and then process them when the timer fires (which happens after the notifications stop flowing in).
Here's my code:
var collectNotificationsTimer: Timer?
var recordIDsFromNotifications = [CKRecordID]()
func processCloudKitNotification(notification: CKNotification, database: CKDatabase){
guard let notification = notification as? CKQueryNotification else { return }
guard let recordID = notification.recordID else { return }
//:: 1 :: Collect record IDs from incoming notifications
if notification.queryNotificationReason == .recordDeleted{
//Delete [-]
//...
}else{
//Add and Edit [+ /]
recordIDsFromNotifications.append(recordID)
//:: 2 :: After collecting IDs for 1 second, request a batch of updated records
collectNotificationsTimer?.invalidate()
collectNotificationsTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false){ _ in
let fetchOperation = CKFetchRecordsOperation(recordIDs: recordIDsFromNotifications)
fetchOperation.fetchRecordsCompletionBlock = { records, error in
recordIDsFromNotifications.removeAll()
if let error = error {
checkCloudKitErrorAndRetryRequest(name: "fetchRecordsCompletionBlock", error: error){}
}else{
if let records = records{
//Save records...
}
}
}
database.add(fetchOperation)
}
}
}

Unique usernames in Firebase

I have been trying to implement Chris’ answer here: Can I make Firebase use a username login process? for the Facebook login but I can’t seem to get my head around it.
So far I’ve tried to set conditions on the textField but as Firebase observer works asynchronously, the conditions to check if the username exists in the database won’t work.
let usernameString = usernameTextField.text
let uid = FIRAuth.auth()?.currentUser?.uid
ref.runTransactionBlock({ (currentData: FIRMutableData) -> FIRTransactionResult in
if var post = currentData.value as? [String : AnyObject], let uid = FIRAuth.auth()?.currentUser?.uid {
let usernamesDictionary = post["usernames"] as! NSDictionary
for (key, _) in usernamesDictionary {
if key as? String == usernameString {
print("username not available: \(key)")
}
else if usernameString == "" {
print("Uh oh! Looks like you haven't set a username yet.")
}
else if key as? String != usernameString {
print("username available: \(key)")
print("All set to go!")
let setValue: NSDictionary = [usernameString!: uid]
post["usernames"] = setValue
currentData.value = post
}
}
return FIRTransactionResult.successWithValue(currentData)
}
return FIRTransactionResult.successWithValue(currentData)
}
Then I tried creating /usernames/ node in the database and set up rules as:
{
"rules": {
"usernames": {
".read": "auth != null",
".write": "newData.val() === auth.uid && !data.exists()"
}
}
}
Now that won’t let me set any username to the database. I get confused in creating rules but my whole point is that I need a sign up flow with the username data that’s unique for each user in the database.
While trying every answer I found in related posts, what worked for me the easy way i.e. without making Firebase rules play a part in it or creating a separate usernames node in the database was to not put an if/else condition inside the Firebase observer but instead to use the exists() method of FIRDataSnapshot.
Now here’s the trick, while I did try only the exists() method with a simple observer but that did not help me. What I did was first query usernames in order, then match the username with queryEqualToValue to filter the query:
refUsers.queryOrderedByChild("username").queryEqualToValue(usernameString).observeSingleEventOfType(.Value , withBlock: {
snapshot in
if !snapshot.exists() {
if usernameString == "" {
self.signupErrorAlert("Uh oh!", message: "Looks like you haven't set a username yet.")
}
else {
// Update database with a unique username.
}
}
else {
self.signupErrorAlert("Uh oh!", message: "\(usernameString!) is not available. Try another username.")
}
}) { error in
print(error.localizedDescription)
}
}
This is the first time out of most of the answers here that worked for me. But for now, I don’t know if this would scale. Post your experiences and best practices. They’ll be appreciated.

CKSubscription error iOS10

My subscriptions were working properly with iOS 9, but since I updated, I have a very odd error. I have two subscription methods that are equal, except for the fields they manage. Here is the code:
let meetingSubscriptionPredicate = Predicate(format: "Users CONTAINS %#", (id?.recordName)!)
let meetingSubscription = CKQuerySubscription(recordType: "Meetings", predicate: meetingSubscriptionPredicate, options: .firesOnRecordCreation)
let notification = CKNotificationInfo()
notification.alertBody = "Meeting Created!"
notification.shouldBadge = true
notification.accessibilityPerformEscape()
meetingSubscription.notificationInfo = notification
database.save(meetingSubscription) { (result, error) -> Void in
if error != nil {
print(error!.localizedDescription)
}
}
let universitiesSubscriptionPredicate = Predicate(format: "Name = %#", self.UniversityTextField.text!)
let universitiesSubscription = CKQuerySubscription(recordType: "Universities", predicate: universitiesSubscriptionPredicate, options: .firesOnRecordCreation)
let universitiesNotification = CKNotificationInfo()
universitiesNotification.alertBody = "Your university is now on Meet'em!"
universitiesNotification.shouldBadge = true
universitiesNotification.accessibilityPerformEscape()
universitiesSubscription.notificationInfo = universitiesNotification
database.save(universitiesSubscription, completionHandler: { (saved, error) in
if error != nil {
print(error!.localizedDescription)
}
else {
print("University subscription created")
}
})
The odd thing is that the Meeting subscription is saved, and the University's subscription is not. I've double checked the names and they are all right at the Dashboard. Besides that, I'm not getting any notification on my phone when supposed to...
Found elsewhere online that you can try to reset your development environment. In my case, the predicate I was trying to use was not set to queryable. This is ticked where you define your record type. The reason it worked one day and not another is possibly due to moving from development to production. At that time you are asked to optimize indexes, and it is possibly here that the ability to search on a given predicate is dropped. That seemed to be the case for me anyway.