Accessing another app's CloudKit database? - ios

Suppose John developed App A and Heather developed App B. They each have different Apple Developer's accounts and they are not on the same team or associated in any way. App B is backed by a public CloudKit database. Is there any way for App A to write to App B's public CloudKit database? Namely, can App A do this:
let DB = CKContainer(identifier: "iCloud.com.Heather.AppB").publicCloudDatabase
and then write to or read from this DB?
I'm guessing that this is not allowed out of the box, but is there a way to set up authentication so that this is possible?

This looks/sounds like the solution you seek.
CloudKit share Data between different iCloud accounts but not with everyone as outlined by https://stackoverflow.com/users/1878264/edwin-vermeer an iCloud specialist on SO.
There is third party explaination on this link too. https://medium.com/#kwylez/cloudkit-sharing-series-intro-4fc82dad7a9
Key steps shamelessly cut'n'pasted ... make sure you read and credit Cory on medium.com!
// Add an Info.plist key for CloudKit Sharing
<key>CKSharingSupported</key>
<true/>
More code...
CKContainer.default().discoverUserIdentity(withPhoneNumber: phone, completionHandler: {identity, error in
guard let userIdentity: CKUserIdentity = identity, error == nil else {
DispatchQueue.main.async(execute: {
print("fetch user by phone error " + error!.localizedDescription)
})
return
}
DispatchQueue.main.async(execute: {
print("user identity was discovered \(identity)")
})
})
/// Create a shred the root record
let recordZone: CKRecordZone = CKRecordZone(zoneName: "FriendZone")
let rootRecord: CKRecord = CKRecord(recordType: "Note", zoneID: recordZone.zoneID)
// Create a CloudKit share record
let share = CKShare(rootRecord: rootRecord)
share[CKShareTitleKey] = "Shopping List” as CKRecordValue
share[CKShareThumbnailImageDataKey] = shoppingListThumbnail as CKRecordValue
share[CKShareTypeKey] = "com.yourcompany.name" as CKRecordValue
/// Setup the participants for the share (take the CKUserIdentityLookupInfo from the identity you fetched)
let fetchParticipantsOperation: CKFetchShareParticipantsOperation = CKFetchShareParticipantsOperation(userIdentityLookupInfos: [userIdentity])
fetchParticipantsOperation.fetchShareParticipantsCompletionBlock = {error in
if let error = error {
print("error for completion" + error!.localizedDescription)
}
}
fetchParticipantsOperation.shareParticipantFetchedBlock = {participant in
print("participant \(participant)")
/// 1
participant.permission = .readWrite
/// 2
share.addParticipant(participant)
let modifyOperation: CKModifyRecordsOperation = CKModifyRecordsOperation(recordsToSave: [rootRecord, share], recordIDsToDelete: nil)
modifyOperation.savePolicy = .ifServerRecordUnchanged
modifyOperation.perRecordCompletionBlock = {record, error in
print("record completion \(record) and \(error)")
}
modifyOperation.modifyRecordsCompletionBlock = {records, recordIDs, error in
guard let ckrecords: [CKRecord] = records, let record: CKRecord = ckrecords.first, error == nil else {
print("error in modifying the records " + error!.localizedDescription)
return
}
/// 3
print("share url \(url)")
}
CKContainer.default().privateDB.add(modifyOperation)
}
CKContainer.default().add(fetchParticipantsOperation)
And on the other side of the fence.
let acceptShareOperation: CKAcceptSharesOperation = CKAcceptSharesOperation(shareMetadatas: [shareMeta])
acceptShareOperation.qualityOfService = .userInteractive
acceptShareOperation.perShareCompletionBlock = {meta, share, error in
Log.print("meta \(meta) share \(share) error \(error)")
}
acceptShareOperation.acceptSharesCompletionBlock = {error in
Log.print("error in accept share completion \(error)")
/// Send your user to wear that need to go in your app
}
CKContainer.default().container.add(acceptShareOperation)
Really I cannot hope to do the article justice, go read it... its in three parts!

If the apps were in the same organization, there is a way to set up shared access. But as you described the situation, it is not possible.

Related

CloudKit, after reinstalling an app how do I reset my subscriptions to current status of records?

I'm dealing with the scenario, where a user has previously deleted the app and has now re-installed it.
It was hitting my delta fetch function, which is receiving a lot of old subscription notifications, mostly deletes. But not downloading current records.
I'm now adding code to perform a fetch on each record type to download all the data.
I'd like to reset delta fetch server token, so the app doesn't have to process old subscriptions notifications. However I can't find how to do this, maybe it's not possible.
Are you referring to CKServerChangeToken (documentation) when you say "delta fetch server token"? And are you attempting to sync within the CloudKit private database?
Assuming that is true, here is an example of how I fetch changes from the private database and keep track of the sync token:
//MARK: Fetch Latest from CloudKit from private DB
func fetchPrivateCloudKitChanges(){
print("Fetching private changes...")
//:::
let privateZoneId = CKRecordZone.ID(zoneName: CloudKit.zoneName, ownerName: CKCurrentUserDefaultName)
/----
let options = CKFetchRecordZoneChangesOperation.ZoneOptions()
options.previousServerChangeToken = previousChangeToken
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: [privateZoneId], optionsByRecordZoneID: [recordZoneID:options])
//Queue up the updated records to process below
var records = [CKRecord]()
operation.recordChangedBlock = { record in
records.append(record)
}
operation.recordWithIDWasDeletedBlock = { recordId, type in
//Process a deleted record in your local database...
}
operation.recordZoneChangeTokensUpdatedBlock = { (zoneId, token, data) in
// Save new zone change token to disk
previousChangeToken = token
}
operation.recordZoneFetchCompletionBlock = { zoneId, token, _, _, error in
if let error = error {
print(error)
}
// Write this new zone change token to disk
previousChangeToken = token
}
operation.fetchRecordZoneChangesCompletionBlock = { error in
if let error = error {
print(error
}else{
//Success! Process all downloaded records from `records` array above...
//records...
}
}
CloudKit.privateDB.add(operation)
}
//Change token property that gets saved and retrieved from UserDefaults
var previousChangeToken: CKServerChangeToken? {
get {
guard let tokenData = defaults.object(forKey: "previousChangeToken") as? Data else { return nil }
return NSKeyedUnarchiver.unarchiveObject(with: tokenData) as? CKServerChangeToken
}
set {
guard let newValue = newValue else {
defaults.removeObject(forKey: "previousChangeToken")
return
}
let data = NSKeyedArchiver.archivedData(withRootObject: newValue)
defaults.set(data, forKey: "previousChangeToken")
}
}
Your specific situation might differ a little, but I think this is how it's generally supposed to work when it comes to staying in sync with CloudKit.
Update
You could try storing the previousServerChangeToken on the Users record in CloudKit (you would have to add it as a new field). Each time the previousServerChangeToken changes in recordZoneFetchCompletionBlock you would have to save it back to iCloud on the user's record.

How to receive CloudKit notifications about changes made on record shared with me?

I have two iCloud accounts (A and B) on two different devices. From one of them (A) I share ckrecord to another one (B) like this:
let controller = UICloudSharingController { controller, preparationCompletionHandler in
let share = CKShare(rootRecord: record)
share[CKShareTitleKey] = "title" as CKRecordValue
share[CKShareTypeKey] = "pl.blueworld.fieldservice" as CKRecordValue
share.publicPermission = .readWrite
let modifyOperation = CKModifyRecordsOperation(recordsToSave: [record, share], recordIDsToDelete: nil)
modifyOperation.savePolicy = .ifServerRecordUnchanged
modifyOperation.perRecordCompletionBlock = { record, error in
print(error?.localizedDescription ?? "")
}
modifyOperation.modifyRecordsCompletionBlock = { records, recordIds, error in
print(share.url)
preparationCompletionHandler(share, CloudAssistant.shared.container, error)
}
CloudAssistant.shared.container.privateCloudDatabase.add(modifyOperation)
}
controller.delegate = self
UIViewController.top()?.present(controller, animated: true)
When second device (B) did accept cloudkit share I fetch record and subscribe for changes:
func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShareMetadata) {
let acceptSharesOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata])
acceptSharesOperation.perShareCompletionBlock = { metadata, share, error in
if let error = error {
UIAlertController.show(withMessage: error.localizedDescription)
} else {
let operation = CKFetchRecordsOperation(recordIDs: [cloudKitShareMetadata.rootRecordID])
operation.perRecordCompletionBlock = { record, _, error in
if let error = error {
UIAlertController.show(withMessage: error.localizedDescription)
} else if let record = record {
CloudAssistant.shared.save(records: [record], recordIDsToDelete: [])
let options: CKQuerySubscriptionOptions = [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
let territorySubscription = CKQuerySubscription(recordType: "Territory", predicate: NSPredicate(value: true), options: options)
let notificationInfo = CKNotificationInfo()
notificationInfo.shouldBadge = false
notificationInfo.shouldSendContentAvailable = true
territorySubscription.notificationInfo = notificationInfo
CloudAssistant.shared.sharedDatabase?.save(territorySubscription) { _, _ in }
}
}
CloudAssistant.shared.container.sharedCloudDatabase.add(operation)
}
}
acceptSharesOperation.qualityOfService = .userInteractive
CKContainer(identifier: cloudKitShareMetadata.containerIdentifier).add(acceptSharesOperation)
}
Now from device A I successfully (I am sure about that, changes is saved in iCloud) perform an update on a record shared with others. But device B doesn't know about that, unless I fetch record manually once again.
But from the other side, it works pretty well.
If I successfully perform an update on a record shared with me (on device B) then device A magically gets a notification about change and everything is fine. What makes the difference?
How to subscribe for changes on a records shared with me?
iOS 11, Swift 4, Xcode 9.
Here's my checklist for debugging subscription notifications not appearing as expected. Sounds like you may have ruled some of these out already.
Make sure the app is registered for notifications
Make sure notifications are enabled on the device for this app
Make sure all devices are using the same container
On the next app startup, read all subscriptions using fetchAllSubscriptionsWithCompletionHandler and NSLog each sub's details (especially: subscriptionID, trigger options, record type and the predicate). Verify that the expected subs exist. Verify that each sub's predicate matches expectations (in this case, compare the predicates you find on both devices).
I wasted a bunch of time debugging "missing" notifications when:
My locally-built version was using the TEST environment and the TestFlight users were accessing the PROD environment. Debugging step 2 found this.
When inadvertently re-using a subscription ID, thus each new sub overwrote the prior one. Debugging step 4 eventually revealed the problem
So far, these four debugging steps have helped me understand all of my "missing notification" problems. Once I understood why the notifs didn't appear, that narrowed down which block of code was responsible.

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)")
})
}
}

Quickblox Enterprise + Swift 3.0 + iOS 10+, QBRequest renaming chat dialog incorrectly

I am attempting to set the name of the chat dialog on the QB server to a custom name. Yet whenever I send the parameters after chatDialog.setValue(roomName, forKey: "name") it renames the private chat dialog to the name of the occupant ID I used to create the room.
let qbUser = QBUUser()
qbUser.id = DataModel.sharedInstance.qbLoginParams.id
qbUser.password = DataModel.sharedInstance.sessionInfo.QBPassword
QBChat.instance().connect(with: qbUser) { (error) in
if error != nil {
print("error: \(String(describing: error))")
return
} else {
print("success connecting to QB chat")
}
let chatDialog: QBChatDialog = QBChatDialog(dialogID: nil, type: QBChatDialogType.private)
chatDialog.occupantIDs = []
//chatDialog.occupantIDs?.append(NSNumber(value: DataModel.sharedInstance.qbLoginParams.id))
chatDialog.occupantIDs?.append(12186)
chatDialog.setValue(roomName, forKey: "name")
print(chatDialog)
QBRequest.createDialog(chatDialog, successBlock: {(response: QBResponse?, createdDialog: QBChatDialog?) in
print("<------------------------------<<< Success creating chat dialog")
print(response ?? "No Response")
print(createdDialog ?? "No Created Dialog")
}, errorBlock: {(response: QBResponse!) in
print("<------------------------------<<< Error creating chat dialog")
print(response ?? "No Response")
})
}
This is perplexing as I clearly set the name of the room before making the request to create and I can see it in the POST parameters in the console yet the response names it incorrectly. Can anyone explain what is happening?
Turns out even though intellisense lets you set the name the DB overwrites it as the opponent name in a type: 3 chat session. I had to switch to a type: 2 or Group type to make it happen.
let chatDialog: QBChatDialog = QBChatDialog(dialogID: nil, type: QBChatDialogType.group)
Apparently you can only access the opponent ID's if you have been added anyways so it wasn't a big deal in my case.

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!

Resources