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

Yes, sharing is available in iOS 10. And yes, the documentation is scarce at the moment. Use WWDC presentation and information available in CloudKit header files.

...and sharing seems to be working great in our test enviroment.


Just look out for a bug on the Mac in -[NSItemProvider registerCloudKitShareWithPreparationHandler...]. It is not working when a _MASReceipt is present.

Could you file a bug report with a small sample project for that bug on the Mac? If you could post the bug number here I'd like to follow up on that.

Well sharing (or something having to do with it) is definitely in iOS 10 because it currently is breaking our ability to fetch any records from our (non-shared) public database's default container... see my post here https://forums.developer.apple.com/thread/62699



We are getting the error:



Error: <CKError 0x15e7c2b0: "Internal Error" (1/5001); "Couldn't get a PCS object to unwrap encrypted data for field encryptedPublicSharingKey: (null)">



Note that it is called the encryptedPublicSharingKey. Since we are not using sharing or anything related to it, then I guess our "sharing" key is (null). But why this would block our ability to even fetch a simple record from the default container of our public database... well that's beyond me.

Did you find a way to share?


I find myself stuck in the first step.

CloudKit says I cannot use default zone in the share database. OK, so I place my records into a custom zone instead. But now I get this error.


CKError(_nsError: <CKError 0x1700514f0: "Partial Failure" (2/1011); "Failed to modify some records"; uuid = C6B66342-B2C4-466C-904C-522EBD8D23E1; container ID = "iCloud.com.jonny.CloudShareTest"; partial errors: {
  Share-F578CD7D-A74D-4E48-8982-3003487E69FB:(MyUniqueRecordZoneName:__defaultOwner__) = <CKError 0x1740561d0: "Invalid Arguments" (12/2006); server message = "ShareDB can't be used to access local zone">
  ... 1 "Batch Request Failed" CKError's omited ...
}>)


I even don't know what is the "local zone". I have already save the zone to private database so it should be a "cloud zone" right?...

OK, just figure out how to share a CKRecord.

It must be done on real device, simulator won't works.


@IBAction func shareButtonItemDidTap(_ sender: UIBarButtonItem) {
   
        let controller = UICloudSharingController { controller, preparationCompletionHandler in
            // this block called when user select a destiniation to share. either copy link, or share to Twitter, Facebook etc.
            print("prepare for sharing")
       
            let privateDatabase = CKContainer.default().privateCloudDatabase
       
            // create and save zone if needed
            let zoneID = CKRecordZoneID(zoneName: "MyUniqueRecordZoneName", ownerName: CKCurrentUserDefaultName)
            let zone = CKRecordZone(zoneID: zoneID)
       
            let modifyRecordZonesOperation = CKModifyRecordZonesOperation(recordZonesToSave: [zone],
                                                                          recordZoneIDsToDelete: nil)
            modifyRecordZonesOperation.timeoutIntervalForRequest = 10
            modifyRecordZonesOperation.timeoutIntervalForResource = 10
            modifyRecordZonesOperation.modifyRecordZonesCompletionBlock = { zone, _, error in
       
                if let error = error as? CKError {
                    print("modifyRecordZonesOperation error:", error)
                    preparationCompletionHandler(nil, nil, error)
                    return
                }
           
                // create, save and share a record. as example, share a simple image record
                let recordID = CKRecordID(recordName: UUID().uuidString, zoneID: zoneID)
                let record = CKRecord(recordType: "Image", recordID: recordID)
           
                record["image"] = UIImagePNGRepresentation(#imageLiteral(resourceName: "sample image"))! as CKRecordValue
           
                // Documentation Note: When saving a newly created CKShare, you must save the share and its rootRecord in the same CKModifyRecordsOperation batch.
                let share = CKShare(rootRecord: record)
                share[CKShareTitleKey] = "My Amazing Image!!" as CKRecordValue
           
                let modifyRecordsOperation = CKModifyRecordsOperation(recordsToSave: [record, share], recordIDsToDelete: nil)
                modifyRecordsOperation.timeoutIntervalForRequest = 10
                modifyRecordsOperation.timeoutIntervalForResource = 10
           
                modifyRecordsOperation.modifyRecordsCompletionBlock = { records, recordIDs, error in
                    if let error = error as? CKError {
                        print("modifyRecordsOperation error:", error)
                    }
                    preparationCompletionHandler(share, CKContainer.default(), error)
                }
                privateDatabase.add(modifyRecordsOperation)
            }
            privateDatabase.add(modifyRecordZonesOperation)
        }
   
        controller.availablePermissions = [.allowPublic, .allowReadOnly]
        controller.popoverPresentationController?.barButtonItem = sender
   
        present(controller, animated: true)
    }
  • A few methods deprecated, but this approach still works. This is the simplest example I have seen. Thanks

  • Spoke too soon. This brings up share windows, but any link sent through email will be a dead link (unavailable or user stopped sharing), and it will not execute imessage.

Add a Comment

I can confirm that CloudKit sharing does not work for 3rd party apps downloaded from the Mac App Store. It is not possible to add participants to a share. There is no such problem with a Developer ID signed app. Bug report 28377984.

Here's my bug reporter number: 28115542


Funny thing is that you can even break the Sierra Notes.app: Simply but a _MASReceipt in there and KABOOM, sharing UI is gone for good :-)

This issue with the Mac App Store signed apps should be addressed in the macOS Sierra beta that was posted today. It would be great to hear from folks that can install it.

Thanks for getting back to us. I will try it out first thing in the morning from the office on one of our testing Macs and report back.


In case it is fixed, any hint if macOS Sierra 10.12.1 will be available shortly for the general public or if it will take several months?

The issue is still there in 10.12.1 Beta 1, I just checked.

Thanks for the example of how to share a record. What steps need to be taken on the particpant (sharee) side?


I presume you have to implement...


func application(_ application: UIApplication, userDidAcceptCloudKitShareWith userAcceptedCloudKitShareWith: CKShareMetadata)


Which gives you a CKShareMetadata object with references to a CKShare, recordID, etc. Do you then need to use a CKAcceptSharesOperation, or has that been taken care of by the framework when the participant user accepted?

Here it is.

Hope you guys can post some sample code, fill API documentations and update CloudKit instructions.


func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShareMetadata) {
        print(#function)

        let time = CFAbsoluteTimeGetCurrent()

        let acceptSharesOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata])
        acceptSharesOperation.perShareCompletionBlock = { metadata, share, error in
            if let error = error {
                print("perShareCompletionBlock error", error)
                return
            }
            print(#function, "\(CFAbsoluteTimeGetCurrent() - time)s")
            self.handleShareMetadata(cloudKitShareMetadata)
        }
        acceptSharesOperation.acceptSharesCompletionBlock = { error in
            if let error = error {
                print("acceptSharesCompletionBlock error", error)
            }
        }

        print("cloudKitShareMetadata.containerIdentifier", cloudKitShareMetadata.containerIdentifier)
        CKContainer(identifier: cloudKitShareMetadata.containerIdentifier).add(acceptSharesOperation)
    }

    func handleShareMetadata(_ metadata: CKShareMetadata) {

        let time = CFAbsoluteTimeGetCurrent()

        let operation = CKFetchRecordsOperation(recordIDs: [metadata.rootRecordID])

        operation.perRecordProgressBlock = { _, progress in
            print("download progress:", progress)
        }
        operation.perRecordCompletionBlock = { record, _, error in
     
            print(#function, "\(CFAbsoluteTimeGetCurrent() - time)s")
     
            if let error = error {
                print("perRecordCompletionBlock error:", error)
            }
            if let record = record,
               let imageData = record["image"] as? Data,
               let image = UIImage(data: imageData) {
                DispatchQueue.main.async {
                   print("Image ckRecord download success!")
                    if let imageView = (self.window?.rootViewController as? UINavigationController)?.viewControllers.first?.view as? UIImageView {
                        imageView.image = image
                    }
                }
            }
        }
        operation.fetchRecordsCompletionBlock = { _, error in
            if let error = error as? CKError {
                print("CKFetchRecordsOperation fetchRecordsCompletionBlock error:", error)
            }
        }

        CKContainer.default().sharedCloudDatabase.add(operation)
    }

Indeed, I apologize. The fix to this issue missed the cutoff for seed 1. It should be in the next seed that goes out. Sorry for the confusion.

Ok, we'll wait for the next seed