How to manage sharing participants with CloudKit?

My objective: I'm trying to run conditional logic on the share button. If I can detect the record is already shared (i.e. record?.share is not nil), then I want to display the participant management screen; else, I want to display the view that allows you to select how you want to share the item.


Video #226 "What's New in CloudKit" for 2016 says you can just call UICloudSharingController with the share, but when I try the code below I get the standard UICloudSharingController view that lets me pick how I want to share the item:


privateDB.fetch(withRecordID: recordID, completionHandler: { (record, error) in
  if error != nil {
    print(error)
  } else {
    let container = CKContainer.default()
    let share = CKShare(rootRecord: record!)

    // This seems like the only way to call UICloudSharingController without a preparation handler
    // as the video suggests
    let shareController = UICloudSharingController(share: share, container: container)
      
    shareController.delegate = self
      
    shareController.availablePermissions = [.allowPrivate, .allowReadWrite]
    shareController.popoverPresentationController?.sourceView = self.shareButton
      
    self.present(shareController, animated: true)
  }
})


I'm also stuck with line 7. How do I create the instance of CKShare I need without creating a new share? Calling record?.share returns a CKReference, not the CKShare I need. But I feel like instantiating a new share isn't the way to go either.

Really struggling without the documentation. If anyone can offer any help, it would go a long way!

Replies

Hi!


Please have a look here:


https://developer.apple.com/reference/uikit/uicloudsharingcontroller


The UICloudSharingController has two init methods: One is for the case where you already have a share. The second init method is if you do not currently have a share. For the second method, a preparation handler is passed to you where you should create and save a share (and the root record!).


Example:

Try to fetch the share of your record zone

-> If no share available, call

UICloudSharingController init(preparationHandler: @escaping (UICloudSharingController, (CKShare?, CKContainer?, Error?) -> Void) -> Void)

-> Create share when preparationHandler is called

-> if share is available, call

UICloudSharingController init(share: CKShare, container: CKContainer)

Hi,


thanks MendelK.

Can you give us more pointers regarding:

- How to fetch a share? As okayjeff was writing, calling record?.share returns a CKReference, not the CKShare

- If no share is available, I call UICloudSharingController init(preparationHandler: @escaping (UICloudSharingController, (CKShare?, CKContainer?, Error?) -> Void) -> Void) but the UICloudSharingController shows "Untitled" because no share has been passed to the init method...

How to proceed here? How to setup CKShareTitleKey and CKShareThumbnailImageDataKey before calling UICloudSharingController init method?

Hi!

You can fetch a share like you fetch any other record in your database. For example, using CKDatabase fetchRecordWithID:<the record id of your share>

Remember, CKShare is a subclass of CKRecord so it behaves like any other kind of CKRecord. You can fetch, modify and delete CKShares the same way you treat records.


Concerning the "Untitled" in UICloudSharingController: Please have a look at the delegate of the UICloudSharingController. There is a method you can provide in the delegate to show a title and a thumbnail for a yet-to-be-created share:


- (nullable NSString *)itemTitleForCloudSharingController:(UICloudSharingController *)csc

- (nullable NSData *)itemThumbnailDataForCloudSharingController:(UICloudSharingController *)csc

thank you so much @MendelK!


btw, do you have any idea what this delegate function is for?


optional func itemType(for csc: UICloudSharingController) -> String?


This is to return a UTI for the share if the share has not yet been created. For example, com.mycompany.myphotoapp.photolibraryalbum may be returned for a share that is sharing a photo album.

I must be overlooking something really obvious, but I'm still stuck.


privateDB.fetch(withRecordID: (list?.recordID)!, completionHandler: { (record, error) in
  if error != nil {
    print("Error finding the shared record: ", error)
  } else {
    let share = CKShare(rootRecord: record!, share: record!.recordID)
    let shareController = UICloudSharingController(share: share, container: self.container)
    self.present(shareController, animated: true, completion: nil)
  }
})


I (think) I understand that CKShare is a subclass of CKRecord, but I'm unable to use a CKRecord ("record") for the "share" argument when calling UICloudSharingController and I'm also unable to cast the CKRecord returned by "fetch" as a CKShare.


The code above simply opens the same sharing modal as if I'm sharing it for the first time. I've checked in the CK Dashboard to confirm and the right record is definitely shared.

By some stroke of luck I just had a crazy idea and it worked. I used the CKReference.recordID that is returned with the original fetch. Since this CKReference refers to the share, I figured I'd be able to downcast from CKRecord to CKShare if I performed another fetch query. Turns out it worked.


Here is the code:


privateDB.fetch(withRecordID: (list?.recordID)!, completionHandler: { (record, error) in
  if error != nil {
    print("Error finding the shared record: ", error)
  } else {
    let shareID = record?.share?.recordID
    self.privateDB.fetch(withRecordID: shareID!, completionHandler: { (share, error) in
      if error != nil {
        print("Error fetching the inner share..", error)
      } else {
        let shareController = UICloudSharingController(share: share as! CKShare, container: self.container)
        self.present(shareController, animated: true, completion: nil)
      }
    })      
  }
})


Going forward I can probably cache the CKReference.recordID on my end so I don't have to perform 2 database queries.


Thank you MendelK!

If you only create one share for your zone, you can use a record name for the CKShare like "MyShare". You do not have to use the generated record name. This way, the recordName and hence the recordID is known and you can directly fetch for it by creating the record ID from the name and zone.

Thank you so much your tips are helping a lot!!


One other question:

* If I share via Message, all thumbnails are ok.

* If I share via email and if the user clicks on the link, he is redirected to iCloud.com, which is great.

The problem is that the iCloud.com page doesn't show the correct thumbnail. It should the standard one (when no icon is setup).

Is there a way to setup the correct one?


When I create the share I set the share like this but obviously it's not working:

if let appIconImage = UIImage(named: "Thumbnail"), let appIconData = UIImagePNGRepresentation(appIconImage) {
    share[CKShareThumbnailImageDataKey] = String(data: appIconData, encoding: .utf8) as CKRecordValue?
}

Hi,


can a total noob ask you how to set this "- (nullable NSString *)itemTitleForCloudSharingController:(UICloudSharingController *)csc" ?


I tried this in Swift (in my ViewController class) but it does not allow me to set a string:


let controller = UICloudSharingController { controller, preparationCompletionHandler in

<some code here>

}

Then I try this, and want to set the title as a string between the brackets, but the compiler wants something else obviously... :


controller.delegate?.itemTitle(for: controller) = "Some Title"


The compiler then throws this error:


"Cannot assign to value: function call returns immutable value"


I am obviously doing something wrong here but I tried a lot of other stuff and this is the closest I get...

The view controller that presents the UICloudSharingController should conform to the UICloudSharingControllerDelegate protocol. Be sure that when you instantiate the sharing controller you also set

sharingController.delegate = self


Conform to protocol:

extension MyViewController: UICloudSharingControllerDelegate {

    func itemTitle(for csc: UICloudSharingController) -> String? {
        return "MyTitle"
    }

    func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) {
        print("Cloud sharing controller did save share")
    }

    func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) {
        print("Cloud sharing controller did stop sharing")
    }

    func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
        print("Failed to save share with error: \(error.localizedDescription)")
    }
}

Hi,

I am able to pull the shareRecords as CKShare but I am not able to access the participants as they are not listed on the normal recordFields. Any help on how to pull them out I would appreciate.

Thanks