I'm trying to save a CKRecord with a location field along with it but when ever I try to it crashes. Here's the code I'm using (crashes where it says: // CRASHES HERE):
func saveStateMood(stateToSave:String) {
// Create CK record
let newRecord:CKRecord = CKRecord(recordType: "State")
let newLocation:CKLocationSortDescriptor = CKLocationSortDescriptor(key: "Loco", relativeLocation: self.theState)
newRecord.setValue(stateToSave, forKey: "State")
newRecord.setValue(newLocation, forKey: "Loco") // CRASHES HERE!!!!!!
// Save record into public database
if let database = self.publicDatabase {
database.saveRecord(newRecord, completionHandler: { (record:CKRecord!, error:NSError!) -> Void in
// Check for error
if error != nil {
// There was an error
NSLog(error.localizedDescription)
}
else {
// There was no error
dispatch_async(dispatch_get_main_queue()) {
// Refresh table
//self.retrieveStateMoods("")
}
}
})
}
}
(I'm using the CLGeocoder to find the current location then I assign the current location to "theState")
I'm trying to follow the CloudKit Quick Start guideline for adding location fields but it is all written in Obj-C and I can't seem to figure it out and I also can't figure out how to fetch the records by their location field either.
You need to store an instance of CLLocation in your record. But you are trying to save an instance of CKLocationSortDescriptor. Update your code to use CLLocation.
Related
I am struggling to understand why my event listener that I initialize on a document is not being triggered whenever I update the document within the app in a different UIViewController. If I update it manually in Google firebase console, the listener event gets triggered successfully. I am 100% updating the correct document too because I see it get updated when I update it in the app. What I am trying to accomplish is have a running listener on the current user that is logged in and all of their fields so i can just use 1 global singleton variable throughout my app and it will always be up to date with their most current fields (name, last name, profile pic, bio, etc.). One thing I noticed is when i use setData instead of updateData, the listener event gets triggered. For some reason it doesn't with updateData. But i don't want to use setData because it will wipe all the other fields as if it is a new doc. Is there something else I should be doing?
Below is the code that initializes the Listener at the very beginning of the app after the user logs in.
static func InitalizeWhistleListener() {
let currentUser = Auth.auth().currentUser?.uid
let userDocRef = Firestore.firestore().collection("users").document(currentUser!)
WhistleListener.shared.listener = userDocRef.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
print("INSIDE LISTENER")
}
}
Below is the code that update's this same document in a different view controller whenever the user updates their profile pic
func uploadProfilePicture(_ image: UIImage) {
guard let uid = currentUser!.UID else { return }
let filePath = "user/\(uid).jpg"
let storageRef = Storage.storage().reference().child(filePath)
guard let imageData = image.jpegData(compressionQuality: 0.75) else { return }
storageRef.putData(imageData) { metadata, error in
if error == nil && metadata != nil {
self.userProfileDoc!.updateData([
"profilePicURL": filePath
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
print("Document successfully updated")
}
}
}
}
}
You can use set data with merge true it doesn't wipe any other property only merge to specific one that you declared as like I am only update the name of the user without wiping the age or address
db.collection("User")
.document(id)
.setData(["name":"Zeeshan"],merge: true)
The answer is pretty obvious (and sad at the same time). I was constantly updating the filepath to be the user's UID therefore, it would always be the same and the snapshot wouldn't recognize a difference in the update. It had been some time since I had looked at this code so i forgot this is what it was doing. I was looking past this and simply thinking an update (no matter if it was different from the last or not) would trigger an event. That is not the case! So what I did was append an additional UUID to the user's UID so that it changed.
I am currently working on a project with a multi user system. The user is able to create new profiles which are saved persistently using CoreData.
My problem is: Only one profile can be the active one at a single time, so I would like to get the ObjectID of the created profile and save it to UserDefaults.
Further I was thinking that as soon as I need the data of the active profile, I can simply get the ObjectID from UserDefaults and execute a READ - Request which only gives me back the result with that specific ObjectID.
My code so far for SAVING THE DATA:
// 1. Create new profile entry to the context.
let newProfile = Profiles(context: context)
newProfile.idProfileImage = idProfileImage
newProfile.timeCreated = Date()
newProfile.gender = gender
newProfile.name = name
newProfile.age = age
newProfile.weight = weight
// 2. Save the Object ID to User Defaults for "activeUser".
// ???????????????????
// ???????????????????
// 3. Try to save the new profile by saving the context to the persistent container.
do {
try context.save()
} catch {
print("Error saving context \(error)")
}
My code so far for READING THE DATA
// 1. Creates an request that is just pulling all the data.
let request: NSFetchRequest<Profiles> = Profiles.fetchRequest()
// 2. Try to fetch the request, can throw an error.
do {
let result = try context.fetch(request)
} catch {
print("Error reading data \(error)")
}
As you can see, I haven't been able to implement Part 2 of the first code block. The new profile gets saved but the ObjectID isn't saved to UserDefaults.
Also Party 1 of the second code block is not the final goal. The request just gives you back all the data of that entity, not only the one with the ObjectID I stored in User Defaults.
I hope you guys have an idea on how to solve this problem.
Thanks for your help in advance guys!
Since NSManagedObjectID does not conform to one of the types handled by UserDefaults, you'll have to use another way to represent the object id. Luckily, NSManagedObjectID has a uriRepresentation() that returns a URL, which can be stored in UserDefaults.
Assuming you are using a NSPersistentContainer, here's an extension that will handle the storage and retrieval of a active user Profile:
extension NSPersistentContainer {
private var managedObjectIDKey: String {
return "ActiveUserObjectID"
}
var activeUser: Profile? {
get {
guard let url = UserDefaults.standard.url(forKey: managedObjectIDKey) else {
return nil
}
guard let managedObjectID = persistentStoreCoordinator.managedObjectID(forURIRepresentation: url) else {
return nil
}
return viewContext.object(with: managedObjectID) as? Profile
}
set {
guard let newValue = newValue else {
UserDefaults.standard.removeObject(forKey: managedObjectIDKey)
return
}
UserDefaults.standard.set(newValue.objectID.uriRepresentation(), forKey: managedObjectIDKey)
}
}
}
This uses a method on NSPersistentStoreCoordinator to construct a NSManagedObjectID from a URI representation.
I am having trouble understanding some of the CloudKit sharing concepts and the WWDC 2016 "What's new in CloudKit" video doesn't appear to explain everything that is required to allow users to share and access shared records.
I have successfully created an app that allows the user to create and edit a record in their private database.
I have also been able to create a Share record and share this using the provided sharing UIController. This can be successfully received and accepted by the participant user but I can't figure out how to query and display this shared record.
The app creates a "MainZone" in the users private database and then creates a CKRecord in this "MainZone". I then create and save a CKShare record and use this to display the UICloudSharingController.
How do I query the sharedDatabase in order to access this record ? I have tried using the same query as is used in the privateDatabase but get the following error:
"ShareDB can't be used to access local zone"
EDIT
I found the problem - I needed to process the accepted records in the AppDelegate. Now they appear in the CloudKit dashboard but I am still unable to query them. It seems I may need to fetch the sharedDatabase "MainZone" in order to query them.
Dude, I got it: First you need to get the CKRecordZone of that Shared Record. You do it by doing the following:
let sharedData = CKContainer.default().sharedCloudDatabase
sharedData.fetchAllRecordZones { (recordZone, error) in
if error != nil {
print(error?.localizedDescription)
}
if let recordZones = recordZone {
// Here you'll have an array of CKRecordZone that is in your SharedDB!
}
}
Now, with that array in hand, all you have to do is fetch normally:
func showData(id: CKRecordZoneID) {
ctUsers = [CKRecord]()
let sharedData = CKContainer.default().sharedCloudDatabase
let predicate = NSPredicate(format: "TRUEPREDICATE")
let query = CKQuery(recordType: "Elder", predicate: predicate)
sharedData.perform(query, inZoneWith: id) { results, error in
if let error = error {
DispatchQueue.main.async {
print("Cloud Query Error - Fetch Establishments: \(error)")
}
return
}
if let users = results {
print(results)
self.ctUsers = users
print("\nHow many shares in cloud: \(self.ctUsers.count)\n")
if self.ctUsers.count != 0 {
// Here you'll your Shared CKRecords!
}
else {
print("No shares in SharedDB\n")
}
}
}
}
I didn't understand quite well when you want to get those informations. I'm with the same problem as you, but I only can get the shared data by clicking the URL... To do that you'll need two functions. First one in AppDelegate:
func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShareMetadata) {
let acceptSharesOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata])
acceptSharesOperation.perShareCompletionBlock = {
metadata, share, error in
if error != nil {
print(error?.localizedDescription)
} else {
let viewController: ViewController = self.window?.rootViewController as! ViewController
viewController.fetchShare(cloudKitShareMetadata)
}
}
CKContainer(identifier: cloudKitShareMetadata.containerIdentifier).add(acceptSharesOperation)
}
in ViewConroller you have the function that will fetch this MetaData:
func fetchShare(_ metadata: CKShareMetadata) {
let operation = CKFetchRecordsOperation(recordIDs: [metadata.rootRecordID])
operation.perRecordCompletionBlock = { record, _, error in
if error != nil {
print(error?.localizedDescription)
}
if record != nil {
DispatchQueue.main.async() {
self.currentRecord = record
//now you have your Shared Record
}
}
}
operation.fetchRecordsCompletionBlock = { _, error in
if error != nil {
print(error?.localizedDescription)
}
}
CKContainer.default().sharedCloudDatabase.add(operation)
}
As I said before, I'm now trying to fetch the ShareDB without accessing the URL. I don't want to depend on the link once I already accepted the share. Hope this helps you!
I need the identifier of a newly created contact directly after the save request. The use case: Within my app a user creates a new contact and give them some attributes (eg. name, address ...) after that he can save the contact. This scenario is working as aspected. My code looks like this:
func createContact(uiContact: Contact, withImage image:UIImage?, completion: String -> Void)
{
let contactToSave = uiContact.mapToCNContact(CNContact()) as! Cnmutablecontawctlet
if let newImage = image
{
contactToSave.imageData = UIImageJPEGRepresentation(newImage, 1.0)
}
request = CNSaveRequest()
request.addContact(contactToSave, toContainerWithIdentifier: nil)
do
{
try self.contactStore.executeSaveRequest(request)
print("Successfully saved the CNContact")
completion(contactToSave.identifier)
}
catch let error
{
print("CNContact saving faild: \(error)")
completion(nil)
}
}
The Contact Object (uiContact) is just an wrapper of CNContact.
In the closure completion I need to return the identifier but on this time I have no access to them, because he is creating by the system after the write process.
One solution could be to fetch the newly saved CNContact with predicate
public func unifiedContactsMatchingPredicate(predicate: NSPredicate, keysToFetch keys: [CNKeyDescriptor]) throws -> [CNContact]
but this seems to me like a bit unclean because this contact could have only a name and more than one could exist. Something like a callback with the created identifier would be nice. But there isnĀ“t.
Is there a other way to solve this problem?
This may be a little late but in case someone needs this.
By using the latest SDK (iOS 11), I was able to get the identifier just by:
NSError *error = nil;
saveReq = [[CNSaveRequest alloc] init];
[saveReq addContact:cnContact toContainerWithIdentifier:containerIdentifier];
if (![contactStore executeSaveRequest:saveReq error:&error]) {
NSLog(#"Failed to save, error: %#", error.localizedDescription);
}
else
{
if ([cnContact isKeyAvailable:CNContactIdentifierKey]) {
NSLog(#"identifier for new contact is: %#", cnContact.identifier);
// this works for me everytime
} else {
NSLog(#"CNContact identifier still isn't available after saving to address book");
}
}
swift 4
This is the way to get contact id when creating contact
do {
try store.execute(saveRequest)
if contactToAdd.isKeyAvailable(CNContactIdentifierKey) {
print(contactToAdd.identifier) // here you are getting identifire
}
}
catch {
print(error)
}
I am trying to save a record to CloudKit but am getting an error. I had seen elsewhere that this was an issue that required knowing how to save but I can't get this to work.
var database:CKDatabase = CKContainer.defaultContainer().publicCloudDatabase
var aRecord:CKRecord!
if self.cloudId == nil {
var recordId:CKRecordID = CKRecordID(recordName: "RecordId")
self.cloudId = recordId // Setup at top
}
aRecord = CKRecord(recordType: "RecordType", recordID: self.cloudId)
aRecord.setObject(self.localId, forKey: "localId")
// Set the normal names etc
aRecord.setObject(self.name, forKey: "name")
var ops:CKModifyRecordsOperation = CKModifyRecordsOperation()
ops.savePolicy = CKRecordSavePolicy.IfServerRecordUnchanged
database.addOperation(ops)
database.saveRecord(aRecord, completionHandler: { (record, error) in
if error != nil {
println("There was an error \(error.description)!")
} else {
var theRecord:CKRecord = record as CKRecord
self.cloudId = theRecord.recordID
}
})
This gives me the error:
There was an error <CKError 0x16d963e0: "Server Record Changed" (14/2017); "Error saving record <CKRecordID: 0x15651730; xxxxxx:(_defaultZone:__defaultOwner__)> to server: (null)"; uuid = 369226C6-3FAF-418D-A346-49071D3DD70A; container ID = "iCloud.com.xxxxx.xxxx-2">!
Not sure, given that I have added CKModifyRecordsOperation. Sadly there is no examples within Apple's documentation. I miss that (which you get on MSDN).
Thanks peeps!
A record can be saved to iCloud using CKDatabase's convenience method saveRecord: or via a CKModifyRecordsOperation. If it's a single record, you can use saveRecord: but will need to fetch the record you'd like to modify using fetchRecordWithID: prior to saving it back to iCloud. Otherwise, it will only let you save a record with a new RecordID. More here.
database.fetchRecordWithID(recordId, completionHandler: { record, error in
if let fetchError = error {
println("An error occurred in \(fetchError)")
} else {
// Modify the record
record.setObject(newName, forKey: "name")
}
}
database.saveRecord(aRecord, completionHandler: { record, error in
if let saveError = error {
println("An error occurred in \(saveError)")
} else {
// Saved record
}
}
The code above is only directionally correct but won't work as is because by the time the completionHandler of fetchRecordWithID returns, saveRecord will have fired already. A simple solution would be to nest saveRecord in the completionHandler of fetchRecordWithID. A probably better solution would be to wrap each call in a NSBlockOperation and add them to an NSOperationQueue with saveOperation dependent on fetchOperation.
This part of your code would be for a CKModifyRecordsOperation and not needed in case you are only updating a single record:
var ops:CKModifyRecordsOperation = CKModifyRecordsOperation()
ops.savePolicy = CKRecordSavePolicy.IfServerRecordUnchanged
database.addOperation(ops)
If you do use a CKModifyRecordsOperation instead, you'll also need to set at least one completion block and deal with errors when conflicts are detected with existing records:
let saveRecordsOperation = CKModifyRecordsOperation()
var ckRecordsArray = [CKRecord]()
// set values to ckRecordsArray
saveRecordsOperation.recordsToSave = ckRecordsArray
saveRecordsOperation.savePolicy = .IfServerRecordUnchanged
saveRecordsOperation.perRecordCompletionBlock { record, error in
// deal with conflicts
// set completionHandler of wrapper operation if it's the case
}
saveRecordsOperation.modifyRecordsCompletionBlock { savedRecords, deletedRecordIDs, error in
// deal with conflicts
// set completionHandler of wrapper operation if it's the case
}
database.addOperation(saveRecordsOperation)
There isn't much sample code yet besides the CloudKitAtlas demo app, which is in Objective-C. Hope this helps.
Generally speaking, you have unitary methods (like saveRecord), which deal with only one record at a time, and mass operations (like CKModifyRecordsOperation), which deal with several records at the same time.
These save operations can be used to save records, or to update records (that is, fetch them, apply changes to them, and then save them again).
SAVE examples:
You create a record and want to save it to CloudKit DB:
let database = CKContainer.defaultContainer().publicCloudDatabase
var record = CKRecord(recordType: "YourRecordType")
database.saveRecord(record, completionHandler: { (savedRecord, saveError in
if saveError != nil {
println("Error saving record: \(saveError.localizedDescription)")
} else {
println("Successfully saved record!")
}
})
You create a bunch of records and you want to save them all at once:
let database = CKContainer.defaultContainer().publicCloudDatabase
// just an example of how you could create an array of CKRecord
// this "map" method in Swift is so useful
var records = anArrayOfObjectsConvertibleToRecords.map { $0.recordFromObject }
var uploadOperation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
uploadOperation.savePolicy = .IfServerRecordUnchanged // default
uploadOperation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordsIDs, error in
if error != nil {
println("Error saving records: \(error.localizedDescription)")
} else {
println("Successfully saved records")
}
}
database.addOperation(uploadOperation)
UPDATE examples:
Usually, you have 3 cases in which you want to update records :
you know the record identifier (generally the recordID.recordName of the record you want to save: in that case,
you will use methods fetchRecordWithID and then saveRecord
you know there is a unique record to update but you don't know its recordID: in that case, you will use a query with method
performQuery, select the (only) one you need and again saveRecord
you are dealing with many records that you want to update: in that case, you will use a query to fetch them all
(performQuery), and a CKModifyRecordsOperation to save them all.
Case 1 - you know the unique identifier for the record you want to update:
let myRecordName = aUniqueIdentifierForMyRecord
let recordID = CKRecordID(recordName: myRecordName)
database.fetchRecordWithID(recordID, completionHandler: { (record, error) in
if error != nil {
println("Error fetching record: \(error.localizedDescription)")
} else {
// Now you have grabbed your existing record from iCloud
// Apply whatever changes you want
record.setObject(aValue, forKey: attributeToChange)
// Save this record again
database.saveRecord(record, completionHandler: { (savedRecord, saveError) in
if saveError != nil {
println("Error saving record: \(saveError.localizedDescription)")
} else {
println("Successfully updated record!")
}
})
}
})
Case 2 - you know there is a record corresponding to your conditions, and you want to update it:
let predicate = yourPredicate // better be accurate to get only the record you need
var query = CKQuery(recordType: YourRecordType, predicate: predicate)
database.performQuery(query, inZoneWithID: nil, completionHandler: { (records, error) in
if error != nil {
println("Error querying records: \(error.localizedDescription)")
} else {
if records.count > 0 {
let record = records.first as! CKRecord
// Now you have grabbed your existing record from iCloud
// Apply whatever changes you want
record.setObject(aValue, forKey: attributeToChange)
// Save this record again
database.saveRecord(record, completionHandler: { (savedRecord, saveError in
if saveError != nil {
println("Error saving record: \(saveError.localizedDescription)")
} else {
println("Successfully updated record!")
}
})
}
}
})
Case 3 - you want to grab multiple records, and update them all at once:
let predicate = yourPredicate // can be NSPredicate(value: true) if you want them all
var query = CKQuery(recordType: YourRecordType, predicate: predicate)
database.performQuery(query, inZoneWithID: nil, completionHandler: { (records, error) in
if error != nil {
println("Error querying records: \(error.localizedDescription)")
} else {
// Now you have grabbed an array of CKRecord from iCloud
// Apply whatever changes you want
for record in records {
record.setObject(aValue, forKey: attributeToChange)
}
// Save all the records in one batch
var saveOperation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
saveOperation.savePolicy = .IfServerRecordUnchanged // default
saveOperation.modifyRecordsCompletionBlock = { savedRecords, deletedRecordsIDs, error in
if error != nil {
println("Error saving records: \(error.localizedDescription)")
} else {
println("Successfully updated all the records")
}
}
database.addOperation(saveOperation)
}
})
Now, that was a lenghty answer to your question, but your code mixed both a unitary save method with a CKModifyRecordsOperation.
Also, you have to understand that, each time you create a CKRecord, CloudKit will give it a unique identifier (the record.recordID.recordName), unless you provide one yourself. So you have to know if you want to fetch an existing record, or create a new one before calling all these beautiful methods :-)
If you try to create a new CKRecord using the same unique identifier as another one, then you'll most certainly get an error.
I had the same error, but I was already fetching the record by ID as Guto described. It turned out I was updating the same record multiple times, and things were getting out of sync.
I have an update-and-save method that gets called by the main thread, sometimes rapidly.
I'm using blocks and saving right away, but if you're updating records quickly you can arrive in a situation where the following happens:
Fetch record, get instance A'.
Fetch record, get instance A''.
Update A' and save.
Update A'' and save.
Update of A'' will fail because the record has been updated on the server.
I fixed this by ensuring that I wait to update the record if I'm in the midst updating it.