Collaboration Preview Image and Title for CKShare When Collaborating With CloudKit

I recently updated our CloudKit collaboration invite codebase to use the new UIActivityController and NSItemProvider invitation as described in Apple's documentation. We previously used UICloudSharingController's init(preparationHandler:), which is since deprecated.

We have all of the previous functionality in place: we successfully create a CKShare, send the invite out, engage the share, and collaborate. However, we cannot get the Messages CKShare preview to use our custom image and title (henceforth referred to as “collaboration metadata”). Previously, while using UICloudSharingController's init(preparationHandler:) to commence the share invite, the collaboration metadata successfully displayed in the Messages conversation. Now, we have a generic icon of our app and “Shared with App-Name" title, leading to a loss of contextual integrity for the invite flow.

My question: How do we make the collaboration metadata appear in the Messages conversation?

Here is our code for creating the UIActivityController, NSItemProvider, CKShare, and other related entities. It encapsulates the entire CloudKit CKShare invite setup. You will note that we do configure the CKShare with metadata, and we do set the LPLinkMetadata on the UIActivityItemsConfiguration. GitHub Gist.

The metadata does successfully appear in the UIActivityController and the CKShare's image and title are available to the person receiving the share once they engage it and open it in our app – but the Messages preview item retains the generic message content. Also please note that this issue does occur in the production environment.

As a final note, examining UICloudSharingController's definition leads me to believe that supplying a UIActivityItemSource is the key to getting correct Messages collaboration metadata in place. My efforts at using an item adhering to UIActivityItemSource in the UIActivityViewController used to send the share did not yield the rich previews and displayed metadata I am aiming for.

Answered by DTS Engineer in 795867022

The preview of a share (CKShare) shown in iMessage does display the value of CKShare.SystemFieldKey.title for me. I confirmed that by using the Sharing Core Data objects between iCloud users sample in the following way:

a. Download the Apple sample mentioned above. Follow the Readme to run it on your iOS devices and confirm that the sample works on your side by adding and sharing a photo.

b. Find PhotoContextMenu.swift in the project and change it in the following way:

  1. Add the UI in menuButtons() to trigger the test code. The code I added is wrapped with // TEST: and // TEST: End.:
    private func menuButtons() -> some View {
        ...
        if PersistenceController.shared.privatePersistentStore.contains(manageObject: photo) {
            #if os(watchOS)
            Button(action: {
                createNewShare(photo: photo)
            }) {
                MenuButtonLabel(title: "New Share", systemImage: "square.and.arrow.up")
            }
            .disabled(isPhotoShared)
            
            // TEST: Using UIActivityViewController to create a new share and obseve the rich preview in iMessage.
            #elseif os(iOS)
            Button(action: {
                createNewShareWithActivityViewController(photo: photo)
            }) {
                MenuButtonLabel(title: "New Share", systemImage: "square.and.arrow.up")
            }
            // TEST: End.
            
            #else
            ShareLink(item: photo, preview: SharePreview("A cool photo to share!")) {
                MenuButtonLabel(title: "New Share", systemImage: "square.and.arrow.up")
            }
            .disabled(isPhotoShared)
            #endif
            ...
        }
  1. Add the following extension to provide the function that creates a share with UIActivityViewController.
// TEST: Using UIActivityViewController to create a new share and obseve the rich preview in iMessage.
#if os(iOS)
import UIKit
extension PhotoContextMenu {
    private func createNewShareWithActivityViewController(photo: Photo) {
        toggleProgress.toggle()
        
        Task { @MainActor in
            let itemProvider = NSItemProvider()
            itemProvider.registerCKShare(container: PersistenceController.shared.cloudKitContainer,
                                         allowedSharingOptions: .standard, preparationHandler: {
                
                let persistentContainer = PersistenceController.shared.persistentContainer
                let (_, share, _) = try await persistentContainer.share([photo], to: nil)
                share[CKShare.SystemFieldKey.title] = "<Your custom share title>"
                return share
            })
            
            if let presentingViewController = rootViewController {
                let activityViewController = UIActivityViewController(activityItems: [itemProvider], applicationActivities: nil)
                presentingViewController.present(activityViewController, animated: true)
            }
        }
    }
    
    private var rootViewController: UIViewController? {
        for scene in UIApplication.shared.connectedScenes {
            if scene.activationState == .foregroundActive,
               let sceneDeleate = (scene as? UIWindowScene)?.delegate as? UIWindowSceneDelegate,
               let window = sceneDeleate.window {
                return window?.rootViewController
            }
        }
        print("\(#function): Failed to retrieve the window's root view controller.")
        return nil
    }
}
#endif
// TEST: End.

c. Build and run the app, and try to add a new photo and share it again.

At step c, I see that the value of share[CKShare.SystemFieldKey.title] is on the preview, as shown in the attached screenshot.

You might try the same flow to check if you see the same result.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Accepted Answer

The preview of a share (CKShare) shown in iMessage does display the value of CKShare.SystemFieldKey.title for me. I confirmed that by using the Sharing Core Data objects between iCloud users sample in the following way:

a. Download the Apple sample mentioned above. Follow the Readme to run it on your iOS devices and confirm that the sample works on your side by adding and sharing a photo.

b. Find PhotoContextMenu.swift in the project and change it in the following way:

  1. Add the UI in menuButtons() to trigger the test code. The code I added is wrapped with // TEST: and // TEST: End.:
    private func menuButtons() -> some View {
        ...
        if PersistenceController.shared.privatePersistentStore.contains(manageObject: photo) {
            #if os(watchOS)
            Button(action: {
                createNewShare(photo: photo)
            }) {
                MenuButtonLabel(title: "New Share", systemImage: "square.and.arrow.up")
            }
            .disabled(isPhotoShared)
            
            // TEST: Using UIActivityViewController to create a new share and obseve the rich preview in iMessage.
            #elseif os(iOS)
            Button(action: {
                createNewShareWithActivityViewController(photo: photo)
            }) {
                MenuButtonLabel(title: "New Share", systemImage: "square.and.arrow.up")
            }
            // TEST: End.
            
            #else
            ShareLink(item: photo, preview: SharePreview("A cool photo to share!")) {
                MenuButtonLabel(title: "New Share", systemImage: "square.and.arrow.up")
            }
            .disabled(isPhotoShared)
            #endif
            ...
        }
  1. Add the following extension to provide the function that creates a share with UIActivityViewController.
// TEST: Using UIActivityViewController to create a new share and obseve the rich preview in iMessage.
#if os(iOS)
import UIKit
extension PhotoContextMenu {
    private func createNewShareWithActivityViewController(photo: Photo) {
        toggleProgress.toggle()
        
        Task { @MainActor in
            let itemProvider = NSItemProvider()
            itemProvider.registerCKShare(container: PersistenceController.shared.cloudKitContainer,
                                         allowedSharingOptions: .standard, preparationHandler: {
                
                let persistentContainer = PersistenceController.shared.persistentContainer
                let (_, share, _) = try await persistentContainer.share([photo], to: nil)
                share[CKShare.SystemFieldKey.title] = "<Your custom share title>"
                return share
            })
            
            if let presentingViewController = rootViewController {
                let activityViewController = UIActivityViewController(activityItems: [itemProvider], applicationActivities: nil)
                presentingViewController.present(activityViewController, animated: true)
            }
        }
    }
    
    private var rootViewController: UIViewController? {
        for scene in UIApplication.shared.connectedScenes {
            if scene.activationState == .foregroundActive,
               let sceneDeleate = (scene as? UIWindowScene)?.delegate as? UIWindowSceneDelegate,
               let window = sceneDeleate.window {
                return window?.rootViewController
            }
        }
        print("\(#function): Failed to retrieve the window's root view controller.")
        return nil
    }
}
#endif
// TEST: End.

c. Build and run the app, and try to add a new photo and share it again.

At step c, I see that the value of share[CKShare.SystemFieldKey.title] is on the preview, as shown in the attached screenshot.

You might try the same flow to check if you see the same result.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Thank you for providing your code – the pithy/focused logic helped me to home in on the problem in my code.

I failed to update the CKShare's metadata after uploading it to the CloudKit server. The returned share (or the share flushed locally) does not hold onto the metadata assigned to the share prior to submission to CloudKit servers.

The metadata has to be reassigned after retrieval of the freshly saved share and prior to the execution of the NSItemProvider's preparationHandler.

–––

Updating the code...

Previous:

let itemProvider = createItemProvider(for: share) {
    try await self.saveNewShare(share, rootRecord: rootRecord, container: AVSCloud.container())
}

Updated:

let itemProvider = createItemProvider(for: share) {
    let savedShare = try await self.saveNewShare(share, rootRecord: rootRecord, container: AVSCloud.container())
    savedShare.updateMetadata(note: note)
    return savedShare
}

Thank you.

Collaboration Preview Image and Title for CKShare When Collaborating With CloudKit
 
 
Q