How to fetch the CKShare object of an already existing share (to get share URL)

1.5k Views Asked by At

I am trying to allow users of my app to be able to retrieve the iCloud sharing link to a record that they have ALREADY shared. I can get the URL when creating the share by using the let share = CKShare(rootRecord: CKRecord) followed by Apple's UISharingController. However, every time I use this method, the old link becomes invalid and kicks others out of the share they have already joined. How can I allow the users (the record owners) to fetch a shareable URL for the records CKShare object at any time?

//Initially shares the record
let share = CKShare(rootRecord: newRecord)
share[CKShare.SystemFieldKey.title] = "Group" as CKRecordValue?        
let modifyRecordsOperation = CKModifyRecordsOperation( recordsToSave: [newRecord, share], recordIDsToDelete: nil)

//Activates the UISharingController (code not shown)
2

There are 2 best solutions below

0
On BEST ANSWER

Assuming the record has already been shared, you can get the URL to invite additional participants with the following:

if let shareReference = record.share {
    database.fetch(withRecordID: shareReference.recordID) { (share, error) in
        let shareURL = (share as? CKShare)?.url
    }
}

If you wanted to show the UICloudSharingController again for an existing share (without it generating a new URL), you could do:

let sharingController = UICloudSharingController { (_, prepareCompletionHandler) in

    func createNewShare() {
        let operationQueue = OperationQueue()
        operationQueue.maxConcurrentOperationCount = 1

        let share = CKShare(rootRecord: record)
        share[CKShare.SystemFieldKey.title] = "Title" as CKRecordValue
        share.publicPermission = .readWrite

        let modifyRecordsOp = CKModifyRecordsOperation(recordsToSave: [share, record], recordIDsToDelete: nil)
        modifyRecordsOp.modifyRecordsCompletionBlock = { records, recordIDs, error in
            prepareCompletionHandler(share, self, error)
        }
        modifyRecordsOp.database = database
        operationQueue.addOperation(modifyRecordsOp)
    }

    if let shareReference = record.share {
        database.fetch(withRecordID: shareReference.recordID) { (share, error) in
            guard let share = share as? CKShare else {
                createNewShare()
                return
            }
            prepareCompletionHandler(share, self, nil)
        }
    } else {
        createNewShare()
    }
}
5
On

It sounds like you've got multiple issues going on here. I'd recommend going through the UICloudSharingController documentation carefully.

The first issue is in the code you've provided you're creating a new share each time, when what you want to do is to create it once and then show that share any time you want to interact with it (including getting the share URL). After you create your share you'll probably want to persist that on device somehow (it can be as simple as storing it in UserDefaults).

The second issue is it sounds like you're not instantiating the UICloudSharingController correctly. When you initially create the CKShare you must use the UICloudSharingController init(preparationHandler:) initializer and create the share INSIDE of that.

As per the documentation for the different initializers (emphasis mine):

Important

You must initialize the controller with the correct initializer method. Do not use init(preparationHandler:) if the CKRecord is already shared. Likewise, do not use init(share:container:) if the CKRecord is not shared. Using the wrong initializer leads to errors when saving the record.

And when you then want to show the shareSheet, you use the UICloudSharingController init(share:container:) initializer, and feed it the share you have persisted to device.

Here's the code provided in the UICloudSharingController documentation for creating the CKShare:

func presentCloudSharingController(_ sender: Any) {  guard
    let barButtonItem = sender as? UIBarButtonItem,
    let rootRecord = self.recordToShare else {
    return
  }

  let cloudSharingController = UICloudSharingController { [weak self] (controller, completion: @escaping (CKShare?, CKContainer?, Error?) -> Void) in
    guard let `self` = self else {
      return
    }
    self.share(rootRecord: rootRecord, completion: completion)
  }

  if let popover = cloudSharingController.popoverPresentationController {
    popover.barButtonItem = barButtonItem
  }
  self.present(cloudSharingController, animated: true) {}
}

func share(rootRecord: CKRecord, completion: @escaping (CKShare?, CKContainer?, Error?) -> Void) {
  let shareRecord = CKShare(rootRecord: rootRecord)
  let recordsToSave = [rootRecord, shareRecord];
  let container = CKContainer.default()
  let privateDatabase = container.privateCloudDatabase
  let operation = CKModifyRecordsOperation(recordsToSave: recordsToSave, recordIDsToDelete: []) 
  operation.perRecordCompletionBlock = { (record, error) in
    if let error = error {
      print("CloudKit error: \(error.localizedDescription)")
    }
  }

  operation.modifyRecordsCompletionBlock = { (savedRecords, deletedRecordIDs, error) in
    if let error = error {
      completion(nil, nil, error)
    } else {
      completion(shareRecord, container, nil)
    }
  }

  privateDatabase.add(operation)
}

And next time you want to show it, it's much more strait forward:

 let share = // yourCKShare
 let container = // yourCKContainer
 let viewController = // yourViewController
 let shareController = UICloudSharingController(share: share, container: container)
 viewController.present(shareController, animated: true)

And finally to answer the URL part of your question, the UICloudSharingController is used for all things sharing - while the controller is presenting the share, there's a "Copy Link" button available to the owner (and possibly the user as well if you allow public access - I haven't looked at that as my stuff is private):

enter image description here