Is CloudKit sharing really in iOS 10?

Is the CloudKit sharing support announced at WWDC16 actually supported in iOS 10? The documentation for CKShare, UICloudSharingController and friends is still (as of Sept 10, 2016) sparse to nonexistent.


I haven't been able to find any overview of it other than the WWDC presentation, which lacks important details and doesn't quite match the API interfaces any more. The sample code released on Sept 7 says "Add a new Sharing tab with CloudKit Sharing examples (Big new feature for WWDC)." but I can't find anything in the code about it. Even a Google search for "ckshare" or "uicloudsharingcontroller" turn up nothing interesting. And I've tried to get it working on my own, but I've only been able to get a few simple cases to sort of work. I see that the iOS 10 Notes app works pretty well, so maybe it's working for some cases.


Am I missing something? Has anyone else had any luck with it?

Replies

_MASReceipt Issue has been fixed in 10.12.1b2! Thank you very much.


Sadly, a much bigger issue popped up using CKShares: https://forums.developer.apple.com/thread/64194


You guys would be my heroes if you can fix this, too 😉

Super helpful! Thank you, Jonny.


One thing that trips me up is the insistence that we share root records and shares simultaneously. What if I already have a root record created and synched to iCloud but I come along later and want to share it? How does the implementation differ from what you have here?

If you already have a root record on CloudKit, do the following: First, query your root record. Afterwards, create a CKShare and save it together with the freshly queried root record using a CKModifyOperation.


I think the misunderstanding is that the root record and the share have to be saved in one modify operation. While this is true, it does not mean that the root record has to be created in that modify operation, just saved.


Please be aware that there are limits when working with CKShare, have a look here: https://forums.developer.apple.com/thread/64533

Immensely helpful. Thank you!


For anyone who stumbles upon this, here is how I went about doing it:


@IBAction func shareButtonTapped(_ sender: UIButton) {
   
    let shareController = UICloudSharingController { shareController,
        preparationCompletionHandler in
    
      let zoneID = CKRecordZoneID(zoneName: "MyCustomZoneName",
          ownerName: CKCurrentUserDefaultName)
      let zone = CKRecordZone(zoneID: zoneID)
     
      let modZonesOp = CKModifyRecordZonesOperation(recordZonesToSave: [zone],
          recordZoneIDsToDelete: nil)
      modZonesOp.timeoutIntervalForRequest = 10
      modZonesOp.timeoutIntervalForResource = 10
      modZonesOp.modifyRecordZonesCompletionBlock = { zone, _, error in
       
        if let error = error as? CKError {
          print("modZonesOp error:", error)
          preparationCompletionHandler(nil, nil, error)
          return
        }
       
        
        // Get the record ID; I include it in the model and capture upon initial save to iCloud
        let recordID = self.list?.recordID

        self.privateDB.fetch(withRecordID: recordID!,
            completionHandler: { (record, error) in
          if error != nil {
            print("Error fetching record:", error)
          } else {
            print("Found record, creating share...")
            let share  = CKShare(rootRecord: record!)
            let listType = self.list?.type.rawValue
            share[CKShareTitleKey] = "\(listType!) List" as CKRecordValue
           
            let modRecordsOp = CKModifyRecordsOperation(recordsToSave: [record!, share],
                recordIDsToDelete: nil)
           
            modRecordsOp.timeoutIntervalForRequest = 10
            modRecordsOp.timeoutIntervalForResource = 10
           
            modRecordsOp.modifyRecordsCompletionBlock = { records, recordIDs, error in
              if error != nil {
                print("modifyRecordsOp error:", error)
              } else {
                print("Successfully modified records")
              }
              preparationCompletionHandler(share, CKContainer.default(), error)
            }
            self.privateDB.add(modRecordsOp)
          }
        })
      }
      self.privateDB.add(modZonesOp)
    }
   
    shareController.availablePermissions = [.allowPrivate, .allowReadWrite]
    shareController.popoverPresentationController?.sourceView = shareButton
   
    present(shareController, animated: true)
   
  }

Hi,


thanks for sharing your code.


I tried this in a testing branch of my app on a real device, but when clicking the resulting link, I get taken to the iCloud website which tells me that this item is unavailable because the user stopped sharing it or that I don't have permission to open it.


Is this normal? Or am I doing something wrong. I copied your code (by typing, to get a better understanding), without changing anything.


The only possible reason which comes up to me is that I'm trying to open the link with the iCloud account that created it... Should I have 2 accounts to test?


Any feedback is highly appreciated!

Yes, you need to use 2 devices, 2 Apple IDs.

Hi,

thanks for your feedback. I sourced a spare iPhone and made a second AppleID, however I still get the same error... Seems like I am missing something, will double-check the code for errors...


I also followed along with the example from WWDC2016, but even Apple's example code does not seem to work properly, especially the part where they set ip the controller with (share: share), this is refused by the compiler somehow.


Guess we really need some proper documentation about this...

Janny,


Thanks for the code snippets. You demonstrate how to share a record using one AppleID, then receiving the share on another device with second AppleID. Though, once the data is received, are we required to save the metadata.rootRecordID in persistent storage in the event the app is exitted/killed. I am unable to perform a CKQuery on the shared database after receiving the shared record ID the first time. So all works well for your code (again thanks), though not being able to query the shared database later is a bit limiting. Means I need to store the recordID's in persistent storage or other. I feel like I am missing something important here. Though, Apple would serve developers well by putting together a functional demo app for the share process going forward.


Thanks for any further guidance.

I don't have further test yet.


But I think you may use CKDatabaseSubscription to subscribe all changes that happen in the share database, then, when you receive CloudKit's remote notification, use CKFetchDatabaseChangesOperation and CKFetchRecordZonesOperation to perform change fetch.


Btw I always find CKOperation is more reliable than CKQuery, so you should try it.

Jonny,


I attempted to create a subscription in the sharedCloudDatabase and received CKError:


<CKError 0x1742514c0: "Invalid Arguments" (12/2006); server message = "Subscription type not supported in SharedDB">

Appears this is unaccepted practice.

Jonny,


Also attempted to place a Subscription on Private DB cloudkit.share as follows:


let shareSubscription = CKQuerySubscription(recordType: "cloudkit.share",

predicate: predicate,

options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion])


Though this doesn't ****** upon any changes and/or deletion of the share. Handled differently then other Private DB subscriptions it appears.

MY guess is that you are setting sharing permissions on the `UICloudSharingController` but perhaps not on the actual `CKShare` too. I made that mistake and when I added permissions onto the `CKShare` object it worked.


let share = CKShare(rootRecord: record)
share.publicPermission = .readWrite

Very strange: four months later and there still doesn't appear to be documentation or sample code from Apple. I'd really like to continue my work using these APIs but I'm very concerned that they've been orphaned. Or maybe they don't really work? Or don't scale? Can anyone from Apple shed some light on this? pdm? morgen?