nested asynchronous fetches with Cloudkit? - ios

I am relatively new to coding for iOS. In the past week, I've been incorporating CloudKit into my project. One thing I want to do is get the current User recordID. Then, using that recordID, I want to fetch that user's actual record. Currently I am using the following code, which seems to work:
self.container.fetchUserRecordID() { recordID, error in
if error == nil {
self.userRecordID = recordID
DispatchQueue.main.async(execute: {
self.db.fetch(withRecordID: self.userRecordID) { record, error in
if error == nil {
self.userRecord = record
} else {
if let error = error as? CKError {
print(error)
}
}
}
})
}
else {
if let error = error as? CKError {
print(error)
}
}
}
This, however, feels messy to me. My question is two-fold: 1) In general, is it acceptable to nest asynchronous calls? 2) If not, is there a better way to accomplish what I want to do in this situation?
Thanks!

Related

Errors using PFUser.getCurrentUserInBackground()

I am using Parse and PFUser in a Swift iOS app, and find myself in a case where PFUser.current() does not do exactly what I want, due to synchronisation issues.
For that reason I am trying to use: PFUser.getCurrentUserInBackground().
I got started with the code below, inspired from what can be found here: https://github.com/BoltsFramework/Bolts-ObjC.
But this document probably being a bit outdated, it does not quite work.
let userCheckTask = PFUser.getCurrentUserInBackground()
userCheckTask.continueWith {
(task: BFTask!) -> BFTask<AnyObject> in
if task.isCancelled() { // Error-1.
// the save was cancelled.
} else if task.error != nil {
// the save failed.
} else {
// the object was saved successfully.
var object = task.result() as PFObject // Error-2.
}
}
The compiler gives me two errors, this one on the line marked "Error-1"
Cannot invoke 'isCancelled' with no arguments
And this other one on the line marked "Error-2"
Expression type 'PFUser?' is ambiguous without more context
I have no idea what kind of argument 'isCancelled' is expecting.
Does anyone know how to fix those?
let userCheckTask = PFUser.getCurrentUserInBackground()
userCheckTask.continueWith {
(task: BFTask) -> BFTask<AnyObject> in
if let e = task.error {
return BFTask(error: e)
} else {
return BFTask(result: task.result)
}
}

CloudKit: Get users firstname/surname

I'm trying to get the users first name using cloud kit however the following code is not getting the users first name and is leaving firstNameFromFunction variable empty. Does anyone know how to achieve this in iOS 10?
let container = CKContainer.default()
container.fetchUserRecordID { (recordId, error) in
if error != nil {
print("Handle error)")
}else{
self.container.discoverUserInfo(
withUserRecordID: recordId!, completionHandler: { (userInfo, error) in
if error != nil {
print("Handle error")
}else{
if let userInfo = userInfo {
print("givenName = \(userInfo.displayContact?.givenName)")
print("familyName = \(userInfo.displayContact?.familyName)")
firstNameFromFunction = userInfo.displayContact?.givenName
}else{
print("no user info")
}
}
})
}
}
the permission screen that comes up when asking for the first time, IMO, is very poorly worded. They need to change that. It says "Allow people using 'your app' to look you up by email? People who know your email address will be able to see that you use this app." This make NO sense. This has nothing to do with asking the user to get their iCloud first name, last name, email address.
Speaking of email address - this and the phone number from the lookupInfo property is missing - i.e. set to nil, even though those values are legit and correct. Filing a bug tonight.
First, you will need to request permission to access the user's information.
Then, you can use a CKDiscoverUserIdentitiesOperation. This is just like any other CKOperation (eg. the modify record operation). You just need to create a new operation with the useridentitylookupinfo. Then you will also need to create a completion block to handle the results.
Here is an example function I created:
func getUserName(withRecordID recordID: CKRecordID,
completion: #escaping (String) -> ()) {
if #available(iOS 10.0, *) {
let userInfo = CKUserIdentityLookupInfo(userRecordID: recordID)
let discoverOperation = CKDiscoverUserIdentitiesOperation(userIdentityLookupInfos: [userInfo])
discoverOperation.userIdentityDiscoveredBlock = { (userIdentity, userIdentityLookupInfo) in
let userName = "\((userIdentity.nameComponents?.givenName ?? "")) \((userIdentity.nameComponents?.familyName ?? ""))"
completion(userName)
}
discoverOperation.completionBlock = {
completion("")
}
CKContainer.default().add(discoverOperation)
} else {
// iOS 10 and below version of the code above,
// no longer works. So, we just return an empty string.
completion("")
}
}
First you need to ask the user for permission to be discovered.
Use CKContainer.default().requestApplicationPermission method passing .userDiscoverability on applicationPermission parameter.
The CKContainer.default().discoverUserInfo method is deprecated on iOS 10. Instead use CKContainer.default().discoverUserIdentity method.
Do something like:
CKContainer.default().requestApplicationPermission(.userDiscoverability) { (status, error) in
CKContainer.default().fetchUserRecordID { (record, error) in
CKContainer.default().discoverUserIdentity(withUserRecordID: record!, completionHandler: { (userIdentity, error) in
print("\(userIdentity?.nameComponents?.givenName)")
print("\(userIdentity?.nameComponents?.familyName)")
})
}
}

CloudKit Sharing

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!

PFQuery always return same results even though Parse server changed

I'm developing an iOS project using Parse.com as backend server.
Basically, I'm currently implementing a very basic feature which just simply retrieve some objects with simple condition.
However, the objects can only be correctly retrieved the first time. No matter how I changed any values in Parse "Core" via Web, I still cannot get updated values by refreshing in the app.
For example, I have a class called "Event", the fields are changed from Parse server, but the result I retrieve are never updated.
let eventServerQuery = Event.query()
// I tried to clear all cached results
PFQuery.clearAllCachedResults()
eventServerQuery?.whereKey(EventFields.Campus.rawValue, equalTo: campus!)
eventServerQuery?.findObjectsInBackgroundWithBlock({ (allEvents, error) -> Void in
self.refreshControl?.endRefreshing()
self.toggleRefreshButtonWithSpinner(false)
if error != nil {
print(error?.localizedDescription)
}else{
if allEvents?.count > 0 {
// Display on the map
for eventObject in allEvents! {
let event = Event.initializeFieldsFromPFObject(eventObject)
self.delegate?.addEventToMap(event)
self.events.append(event)
print("\(event.updatedAt)")
print("\(event.title) has \(event.numberOfTasks) tasks")
}
// Event TVC data source
self.tableView.reloadData()
}
}
})
If I delete the app in my device and run the project again, it will of course reload everything from scratch, so that the data will become correct again...
Any help will be appreciated!
Finally, I worked out by myself. I found that whenever the PFObject was pinned, its fields will not be updated. The solution is that the object need to be unpinned before retrieve from server.
Event.unpinAllInBackground(events, block: { (success, error) -> Void in
if error != nil {
print(error?.localizedDescription)
}else{
self.events.removeAll()
let eventServerQuery = Event.query()
eventServerQuery?.whereKey(EventFields.Campus.rawValue, equalTo: self.campus!)
eventServerQuery?.findObjectsInBackgroundWithBlock({ (allEvents, error) -> Void in
print("Debug: retrieving events from server")
self.refreshControl?.endRefreshing()
self.toggleRefreshButtonWithSpinner(false)
if error != nil {
print(error?.localizedDescription)
}else{
if allEvents?.count > 0 {
// Display on the map
for eventOnline in allEvents! {
let event: Event = eventOnline as! Event
event.pinInBackground()
self.delegate?.addEventToMap(event)
self.events.append(event)
}
// Event TVC data source
self.tableView.reloadData()
}
}
})
}
})
Welcome to add comments here regarding the internal logic of Parse library, as sometimes it is not quite clear I think.

Swift CloudKit SaveRecord "Error saving record"

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.

Resources