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.
Related
class UsersViewModel : ObservableObject {
#Published var users = [CurrentUser]()
init() {
fetchUserLists()
print(users)
}
func fetchUserLists() {
FirebaseManager.shared.firestore.collection("users")
.getDocuments { documentSnapshot, error in
if let error = error {
print("Error to get user lists")
return
}
//success
documentSnapshot?.documents.forEach({ snapshot in
let user = try? snapshot.data(as: CurrentUser.self)
if user?.uid != FirebaseManager.shared.auth.currentUser?.uid {
self.users.append(user!)
}
})
}
}
}
I'm trying to fetch all of users in my firestore database, but unfortunately my users array is empty. I don't know what my mistake is.
Please check my firestore screen shot, and give me tips!
Thank you!
You're having an issue with asynchronous code. Code is faster than the internet and you have to allow time for data to be retrieved from Firebase.
Additionally, Firebase data is only valid within the closure following the Firebase call. In this case your code is attempting to print an array before it's been filled.
Here's the issue
init() {
fetchUserLists() //<-takes time to complete
print(users) //this is called before fetchUserLists fills the array
}
here's the fetchUserLists function with where the print statement should be
func fetchUserLists() {
FirebaseManager.shared.firestore.collection("users").getDocuments { documentSnapshot, error in
if let error = error {
print("Error to get user lists")
return
}
documentSnapshot?.documents.forEach({ snapshot in
let user = try? snapshot.data(as: CurrentUser.self)
if user?.uid != FirebaseManager.shared.auth.currentUser?.uid {
self.users.append(user!)
}
})
print(self.users) //we are within the closure and the array is now populated
//this is a good spot to, for example, reload a tableview
// or update other UI elements that depend on the array data
}
}
I am trying to write some entries to a Firebase Realtime database updateChildValues (...). Things are working just fine unless I add completion block to the command.
Anyone having faced similar issues?
This code works just fine:
private func createCalendarEntry(userId: String){
var dbCalendarEntry = [String: Any]()
let ref = Database.database().reference().child("calendar").child(userId)
dbCalendarEntry["name"] = event.name
ref.updateChildValues(dbCalendarEntry)
}
Adding the completion block, nothing happens:
private func createCalendarEntry(userId: String), completion: ((Error?,String?) -> ())?) {
ref.updateChildValues(dbCalendarEntry, withCompletionBlock: { (error, reference) in
if error != nil {
print("Error updating data:")
completion?(error, ref.key)
}
else
{
print("No Error")
completion?(nil,nil)
}
})
}
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.
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.
I am trying to read the value of a Firestore document. I have tried doing it two different ways, but each fails.
In the first one, an error is thrown on the return line: Unexpected non-void return value in void function. I found out why this happened, and so, I implemented the second way.
import UIKit
import Firestore
func readAvailableLists(forUser user: String) -> [String] {
let db = Firestore.firestore()
db.collection("userslist").document(user).getDocument { (document, err) in
if let document = document, document.exists {
return UserInformationDocument(dictionary: document.data()!)?.lists!
} else {
print("Document does not exist")
}
}
}
In the second method, I assign the UserInformationDocument(dictionary: document.data()!)?.lists! to a variable and return that variable at the end of the function (see code below). However, when I do this, it the function returns an empty array. What surprises me is that the print return the correct value, but after long after the function has executed the return statement. Is it because it is an async demand? And if so, how should I fix this?
import UIKit
import Firestore
func readAvailableLists(forUser user: String) -> [String] {
let db = Firestore.firestore()
var firestoreUserDocument: [String] = []
db.collection("userslist").document(user).getDocument { (document, err) in
if let document = document, document.exists {
firestoreUserDocument = (UserInformationDocument(dictionary: document.data()!)?.lists!)!
print((UserInformationDocument(dictionary: document.data()!)?.lists!)!)
} else {
print("Document does not exist")
}
}
return firestoreUserDocument
}
The Firebase call is an asynchronous function. It takes extra time to execute because it's talking to a server (as you've noted) - as a result, the completion block (the block that defines document and err in your example) happens at a different time, outside of the rest of the body of the function. This means you can't return a value from inside it, but you can pass another closure through to it, to execute later. This is called a completion block.
func readAvailableLists(forUser user: String, completion: #escaping ([String]?, Error?) -> Void) -> [String] {
let db = Firestore.firestore()
db.collection("userslist").document(user).getDocument { (document, err) in
if let document = document, document.exists {
// We got a document from Firebase. It'd be better to
// handle the initialization gracefully and report an Error
// instead of force unwrapping with !
let strings = (UserInformationDocument(dictionary: document.data()!)?.lists!)!
completion(strings, nil)
} else if let error = error {
// Firebase error ie no internet
completion(nil, error)
}
else {
// No error but no document found either
completion(nil, nil)
}
}
}
You could then call this function elsewhere in your code as so:
readAvailableLists(forUser: "MyUser", completion: { strings, error in
if let strings = strings {
// do stuff with your strings
}
else if let error = error {
// you got an error
}
})