-
Integrate your custom collaboration app with Messages
Discover how the SharedWithYou framework can augment your app's collaboration infrastructure. We'll show you how to send secure invitations to collaborative content and synchronize participant changes. We'll also cover displaying content updates within the relevant conversation. For an introduction to SharedWithYou, watch "Add Shared with You to your app" from WWDC22. For an overview of the collaboration UI APIs, watch "Enhance collaboration experiences with Messages" from WWDC22. (Note: API will be available in an upcoming beta.)
Resources
Related Videos
WWDC22
-
0:00
♪ Mellow instrumental hip-hop music ♪ ♪ Devin Clary: Hi. I'm Devin, an engineer on the Messages team.
0:14
0:17
Devin: Welcome to "Integrate your custom collaboration app with Messages." Collaboration starts with a conversation, and in iOS 16 and macOS Ventura, you can bring your app's custom collaboration experience right into the fabric of the conversation.
0:32
0:36
0:42
Next, we'll give you everything you need to instantly verify recipient access, and respond to participant changes, all without compromising privacy.
0:52
Finally, we'll show you how your app can post notices about the content right to the Messages conversation.
0:59
This video assumes your app has existing collaboration infrastructure, and has already adopted universal links.
1:06
We'll also build on some concepts introduced in "Add Shared with You to your app" and "Enhance collaboration experiences with Messages." First up, I'll go over the life cycle of a custom collaboration message to illustrate how this API allows your users to start collaborating faster than ever.
1:24
When a user decides to share a collaboration from your app through Messages, you first create metadata to represent the content.
1:33
The metadata includes share options the user can configure prior to sending the message, and a number of other properties you can customize.
1:41
1:46
1:51
1:55
2:00
This is useful if your app's link creation depends on the selected share options, or recipients, as configured in the Messages compose field.
2:08
2:14
Before the message is sent, Messages asks your app for the universal link and a device independent identifier for the content.
2:21
Using that identifier, Messages provides a set of cryptographic identities representing the recipients of that specific collaboration message.
2:29
Your app will use these identities later to allow the recipients to immediately open the link on any of their devices.
2:38
2:44
2:50
2:53
The goal is to instantly verify access, pairing a recipient identity with an account on your server.
2:59
When the recipient opens the link, your app receives a call to open the URL, just like it does for any other link.
3:08
When your app detects that a user account doesn't yet have access to the document, it queries the system for a proof of user identity cryptographically signed by the recipient device.
3:18
3:23
If the signature is valid, the server compares the proof against the identities previously provided by the sending device.
3:30
3:35
And with that, the recipient has gained instant and secure access to the content, all without exchanging account information! And that's the life cycle of a collaboration message! Next, let's look more closely at the API for starting a collaboration.
3:51
3:55
4:01
This class has a few properties for you to configure: the content's title, a local identifier to reference the content before its been shared; the initiator name and account handle, to provide transparency to the user about the account they're sharing from; and the default share options, for the user to configure.
4:21
4:27
4:33
The string only needs to be sufficient for your app to identify the content locally, not across devices.
4:41
4:47
Set the content title, the initiator's account handle, and their name using PersonNameComponents from the foundation framework.
4:57
The handle and name are only displayed locally so the user can confirm the account they're sharing from.
5:04
5:07
5:12
Share options are the settings a user configures on the collaboration in Messages or the share sheet.
5:18
5:23
Share options might include settings like who can make edits to a collaboration or who should have access to the content.
5:32
5:38
Depending on how they're grouped, options represent individual switches, or mutually exclusive values for a setting.
5:46
5:53
There are two classes to represent a group of options: SWCollaborationOptionsGroup and SWCollaborationOptions PickerGroup.
6:04
You use SWCollaborationOptionsGroup to represent a collection of switches, while SWCollaborationOptions PickerGroup represents mutually exclusive values for a setting.
6:16
Finally, SWCollaborationShareOptions defines the full set of option groups, to be set on the metadata's defaultShareOptions property.
6:25
6:29
6:34
6:38
6:43
The identifier is an arbitrary string you later use to identify which option was selected by the user.
6:50
6:54
7:00
7:06
7:11
7:16
But since this is a generic option group, the user will be able to configure whether to allow mentions and comments independently.
7:23
Finally, the two option groups are used to initialize an instance of SWCollaborationShareOptions, which is then set on the metadata.
7:34
Next, the metadata is provided to the share sheet or drag and drop, depending on how the user decides to share the content.
7:42
7:49
Watch "Meet Transferable" and "What's new in SwiftUI" to learn more about Transferable and ShareLink.
7:58
Here's how easy it is to support collaboration on a proxy representation in SwiftUI! From within a Transferable model object, set up a ProxyRepresentation to return a collaboration metadata instance.
8:15
8:22
8:28
8:33
8:39
It's also good practice to register multiple representations of the content to support sharing through as many channels as possible.
8:47
For example, Messages automatically offers an option to send the content as a copy if you provide a file representation.
8:55
You'll use the NSItemProvider API with UIActivityViewController and UIDragItem on iOS and iPadOS and NSSharingServicePicker on macOS.
9:08
9:12
9:16
Register the collaboration metadata created in the previous example, with visibility set to all processes on the system.
9:25
Initialize UIActivityItemsConfiguration with the item provider, then initialize UIActivityViewController with that configuration.
9:36
9:40
9:44
Initialize NSItemProvider and register the metadata the same way, then create a UIDragItem with the item provider to use with the drag and drop APIs.
9:58
10:03
10:06
10:11
10:17
10:22
10:29
Using that extension, set the collaboration metadata directly on a new NSPasteboardItem instance in order to support drag and drop.
10:39
And that's all you need for a draft of your collaborative content to be staged in Messages! Next, when the user taps the send button, the system coordinates with your app to set up the share.
10:52
10:57
11:03
That shared instance coordinates the collaboration through a delegate you define called an actionHandler.
11:09
To ensure your app is always available to coordinate collaborations, it will be launched in the background when needed.
11:15
So you should register the delegate soon after launch and handle actions immediately to avoid timeouts.
11:22
11:28
11:33
Then, in the app delegate's didFinishLaunchingWithOptions method, set the actionHandler property to an object that conforms to the SWCollaborationActionHandler protocol.
11:44
11:49
11:53
11:59
12:03
SWStartCollaborationAction contains the collaboration metadata you set up earlier, updated with the user's selected share options.
12:12
Once you've performed the necessary setup, you fulfill the start action with the universal link and a device-independent identifier for the collaboration.
12:20
12:26
12:32
First, retrieve the local identifier, and user-selected share options from the action's metadata property.
12:40
12:47
12:50
12:54
Finally, fulfill the action with the universal link and the device independent identifier from the response.
13:01
13:05
If the start action was successful, the system sends your app a second action to update the collaboration participants.
13:13
The SWUpdateCollaboration ParticipantsAction contains the cryptographic identities for the participants.
13:19
The identities are derived from the collaboration identifier fulfilled by the start action in the previous step.
13:26
13:30
13:35
13:40
13:45
13:49
13:53
Next, retrieve the participant data to store on your servers using the action's addedIdentities property.
14:01
14:05
14:09
14:16
Set up another server request, this time to add the participants to the collaboration with the target identifier.
14:23
14:29
14:34
Now that you've set up the collaboration, your app has everything it needs to grant immediate access to the recipients of the message.
14:42
I'll hand it over to Lance to show you how to do that! Lance: Thanks, Devin. In this section, I'll show how to provide immediate access to the recipients using the identity data you stored on your server in the previous step.
14:56
15:02
15:08
15:14
15:16
When a collaboration message is sent, it's actually sent individually to each of a person's devices.
15:24
15:29
Since the goal is to allow access only on this set of devices, the root hash is derived from the set of public keys registered to each recipient.
15:39
15:45
15:51
In order to derive an identity for the user based on their public keys, the keys are used as the leaves of this tree.
15:59
The hashing algorithm used in the Merkle tree ensures that the root node can only be computed from that set of keys.
16:07
16:15
The keys will be unique for each collaboration identifier provided by your app, using a process called key diversification.
16:24
To prevent tracking the number of devices registered to a user, the set is padded with random keys up to a fixed size.
16:31
16:38
16:44
16:51
17:00
17:09
Notice that it's possible to generate a root hash using a subset of the nodes from a complete Merkle tree.
17:17
The root hash in this tree can be reproduced using just the hashes H4, 7, and 11, along with the diversified public key P3.
17:29
17:35
17:39
17:44
17:49
It's important to note that you can prove the public key P3 was used to generate a given root hash, without needing to reconstruct the entire tree.
17:59
18:05
18:10
18:15
18:22
18:28
18:35
To perform verification, you'll first generate this object along with a cryptographic signature to send to your server.
18:43
18:48
18:57
Use the signature to ensure the request cannot be replayed by a bad actor to gain access to your collaboration.
19:05
19:13
19:16
19:22
19:28
19:36
Next, I'll request the challenge from my server, and pass the data I get back to the getSignedIdentityProof method on SWHighlightCenter, along with the highlight.
19:50
19:53
19:58
20:03
20:09
20:16
The data is signed using the elliptic curve digital signature algorithm over the P-256 elliptic curve, using SHA256 as a hash function.
20:27
20:32
20:38
Once you have verified the signature, you can trust that the identity proof was sent from the device associated with that public key.
20:46
20:52
Here is an example of what an SWPersonIdentityProof would contain using the example tree we looked at before.
20:59
21:04
21:06
21:12
21:19
21:26
A recursive algorithm works nicely when working with tree data structures, so that's what I've done here.
21:32
On the initial invocation, pass in the hash of the public key, the set of inclusion hashes, and the public key index.
21:44
21:49
21:56
22:03
Next, the consumed node in the inclusionHashes array is removed, and the rest are passed to a recursive call to this same function.
22:14
22:20
22:28
The server can now check that this generated root hash is in the list of root hashes the owner of the document uploaded during sending.
22:36
22:42
Now you can grant access to the document with confidence! To recap the steps you'll follow to verify an identity: first, look up the collaboration highlight for your content while handling its universal link.
22:55
22:59
23:04
23:07
23:11
23:19
Now that you know all about verifying access to your collaboration links, I'll talk about how to coordinate participant changes with Messages.
23:28
When the participants in a Messages group change, and that group is collaborating together, a user can choose to propagate those changes to your app, right from a banner in the Messages thread.
23:40
In this scenario, your app receives another SWUpdateCollaboration ParticipantsAction containing the added and removed identities.
23:50
You'll use the same code you wrote to handle this action when setting up a collaboration, but you'll also need to handle removed participants.
24:00
24:06
24:12
24:19
This example uses the removed identities property on the action and passes them to a similar removal API request.
24:28
Note that this code only shows handling removed identities, but a complete implementation should handle both added and removed identities.
24:38
And that's all you need to handle participant changes! Lastly, when changes are made to a collaboration, your app posts notices about those changes to be shown directly in Messages.
24:50
24:55
25:01
25:07
25:12
25:17
25:24
25:31
25:36
A change event for content updates or comments, a membership event when a participant joins or leaves, a mention event when a user is mentioned in a collaboration, and a persistence event when content is moved or deleted.
25:54
25:59
26:06
Remember, this identifier is one you defined during the collaboration initiation, so your app should have this available for use when a content change is made.
26:17
26:21
26:30
26:36
Similarly, for membership changes, post a membership event, this time passing the addedCollaborator or removedCollaborator trigger type.
26:49
26:55
27:00
Recall that you associated a person identity with a user account in your app while verifying access.
27:07
Then, post the mention event in the same way, this time passing the mentioned identity as a parameter.
27:15
27:21
27:29
27:37
And that is how your app can notify collaborators, and they will get those updates right in Messages.
27:46
Devin: And with that, you're ready to integrate your app's collaboration experience with messages by following a few steps.
27:52
Set up your content to be shared collaboratively, cryptographically verify participant access, keep track of participant changes, and post notices in Messages to connect your users right back to the content.
28:06
Be sure to check out the "Enhance collaboration experiences with Messages" video to learn more about the new UI elements you can display for collaborations.
28:15
Lance: We can't wait to get collaborating with your apps! Devin and Lance, cryptographically signing off.
28:20
-
-
4:21 - Configure SWCollaborationMetadata
let localIdentifier = SWLocalCollaborationIdentifier(rawValue: "identifier") let metadata = SWCollaborationMetadata(localIdentifier: localIdentifier) metadata.title = "Content Title" metadata.initiatorHandle = "user@example.com" let formatter = PersonNameComponentsFormatter() if let components = formatter.personNameComponents(from: "Devin") { metadata.initiatorNameComponents = components } metadata.defaultShareOptions = ...
-
6:34 - Configure SWCollaborationShareOptions
let permission = SWCollaborationOptionsPickerGroup(identifier: UUID().uuidString, options: [ SWCollaborationOption(title: "Can make changes", identifier: UUID().uuidString), SWCollaborationOption(title: "Read only", identifier: UUID().uuidString) ]) permission.options[0].isSelected = true permission.title = "Permission" let additionalOptions = SWCollaborationOptionsGroup(identifier: UUID().uuidString, options: [ SWCollaborationOption(title: "Allow mentions", identifier: UUID().uuidString), SWCollaborationOption(title: "Allow comments", identifier: UUID().uuidString) ]) additionalOptions.title = "Additional Settings" let optionsGroups = [permission, additionalOptions] metadata.defaultShareOptions = SWCollaborationShareOptions(optionsGroups: optionsGroups)
-
7:58 - SWCollaborationMetadata SwiftUI TransferRepresentation
struct CustomCollaboration: Transferable { var name: String static var transferRepresentation: some TransferRepresentation { ProxyRepresentation { customCollaboration in SWCollaborationMetadata( localIdentifier: .init(rawValue: "com.example.customcollaboration"), title: customCollaboration.name, defaultShareOptions: nil, initiatorHandle: "johnappleseed@apple.com", initiatorNameComponents: nil ) } } }
-
8:16 - Using a collaboration metadata TransferRepresentation with ShareLink
struct ContentView: View { var body: some View { ShareLink(item: CustomCollaboration(name: "Example"), preview: .init("Example")) } }
-
9:08 - iOS Share Sheet
func presentActivityViewController(metadata: SWCollaborationMetadata) { let itemProvider = NSItemProvider() itemProvider.registerObject(metadata, visibility: .all) let activityConfig = UIActivityItemsConfiguration(itemProviders: [itemProvider]) let shareSheet = UIActivityViewController(activityItemsConfiguration: activityConfig) present(shareSheet, animated: true) }
-
9:42 - iOS Drag and Drop
func createDragItem(metadata: SWCollaborationMetadata) -> UIDragItem { let itemProvider = NSItemProvider() itemProvider.registerObject(metadata, visibility: .all) return UIDragItem(itemProvider: itemProvider) }
-
9:58 - macOS Sharing Popover
func showSharingServicePicker(view: NSView, metadata: SWCollaborationMetadata) { let itemProvider = NSItemProvider() itemProvider.registerObject(metadata, visibility: .all) let picker = NSSharingServicePicker(items: [itemProvider]) picker.show(relativeTo: view.bounds, of: view, preferredEdge: .minY) }
-
10:18 - macOS Drag and Drop NSPasteboardItem extension
func createPasteboardItem(metadata: SWCollaborationMetadata) -> NSPasteboardItem { let pasteboardItem = NSPasteboardItem() pasteboardItem.collaborationMetadata = metadata return pasteboardItem }
-
11:22 - Set up SWCollaborationCoordinator
private let collaborationCoordinator = SWCollaborationCoordinator.shared func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?) -> Bool { // Conform to the SWCollaborationActionHandler protocol collaborationCoordinator.actionHandler = self }
-
12:27 - SWStartCollaborationAction
func collaborationCoordinator(_ coordinator: SWCollaborationCoordinator, handle action: SWStartCollaborationAction) { let localID = action.collaborationMetadata.localIdentifier.rawValue let selectedOptions = action.collaborationMetadata.userSelectedShareOptions let prepareRequest = APIRequest.PrepareCollaboration(localID: localID, selectedOptions) Task { do { let response = try await apiController.send(request: prepareRequest) let identifier = response.deviceIndependentIdentifier action.fulfill(using: response.url, collaborationIdentifier: identifier) } catch { Log.error("Caught error while preparing the collaboration: \(error)") action.fail() // cancels the message } } }
-
13:40 - SWUpdateCollaborationParticipantsAction
func collaborationCoordinator(_ coordinator: SWCollaborationCoordinator, handle action: SWUpdateCollaborationParticipantsAction) { let identifier = action.collaborationMetadata.collaborationIdentifier let participants: [Data] = action.addedIdentities.compactMap { $0.rootHash } let addParticipants = APIRequest.AddParticipants(identifier: identifier, participants) Task { do { try await apiController.send(request: addParticipants) action.fulfill() // sends the URL provided by the start action } catch { Log.error("Caught error while adding participants to collaboration: \(error)") action.fail() // cancels the message } } }
-
19:12 - Retrieve a signed identity proof for a highlight
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { let highlightCenter: SWHighlightCenter = self.highlightCenter let challengeRequest = APIRequest.GetChallengeData() Task { do { let highlight = try highlightCenter.collaborationHighlight(for: url) let challenge = try await apiController.send(request: challengeRequest) let proof = try await highlightCenter.getSignedIdentityProof(for: highlight, using: challenge.data) let proofOfInclusionRequest = APIRequest.SubmitProofOfInclusion(for: proof) let result = try await apiController.send(request: proofOfInclusionRequest) documentController.update(currentDocument, with: result) } catch { Log.error("Caught error while generating proof of inclusion: \(error)") } } }
-
21:20 - Example code for root hash generation
func generateRootHashFromArray(localHash: SHA256Digest, inclusionHashes: [SHA256Digest], publicKeyIndex: Int) -> SHA256Digest { guard let firstHash = inclusionHashes.first else { return localHash } // Check if the node is the left or the right child let isLeft = publicKeyIndex.isMultiple(of: 2) // Calculate the combined hash var rootHash: SHA256Digest if isLeft { rootHash = hash(concatenate([localHash, firstHash]), using: .sha256) } else { rootHash = hash(concatenate([firstHash, localHash]), using: .sha256) } // Recursively pass in elements and move up the Merkle tree let newInclusionHashes = inclusionHashes.dropFirst() rootHash = generateRootHashFromArray( localHash: rootHash, inclusionHashes: Array(newInclusionHashes), publicKeyIndex: (publicKeyIndex / 2) ) return rootHash }
-
24:12 - SWUpdateCollaborationParticipantsAction - removing participants
func collaborationCoordinator(_ coordinator: SWCollaborationCoordinator, handle action: SWUpdateCollaborationParticipantsAction) { // Example of removing participants only. Handle the added identities here too. let identifier = action.collaborationMetadata.collaborationIdentifier let removed: [Data] = action.removedIdentities.compactMap { $0.rootHash } let removeParticipants = APIRequest.RemoveParticipants(identifier: identifier, removed) Task { do { try await apiController.send(request: removeParticipants) action.fulfill() } catch { log.error("Caught error while adding participants to collaboration: \(error)") action.fail() } } }
-
25:54 - Post an SWHighlightChangeEvent Notice
func postContentEditEvent(identifier: SWCollaborationIdentifier) throws { let highlightCenter: SWHighlightCenter = self.highlightCenter let highlight = try highlightCenter.collaborationHighlight(forIdentifier: identifier) let editEvent = SWHighlightChangeEvent(highlight: highlight, trigger: .edit) highlightCenter.postNotice(for: editEvent) }
-
26:39 - Post an SWHighlightMembershipEvent Notice
func postContentEditEvent(identifier: SWCollaborationIdentifier) throws { let highlightCenter: SWHighlightCenter = self.highlightCenter let highlight = try highlightCenter.collaborationHighlight(forIdentifier: identifier) let editEvent = SWHighlightChangeEvent(highlight: highlight, trigger: .edit) highlightCenter.postNotice(for: editEvent) }
-
26:50 - Post an SWHighlightMentionEvent Notice
func postMentionEvent(identifier: SWCollaborationIdentifier, mentionedRootHash: Data) throws { let mentionedIdentity = SWPerson.Identity(rootHash: mentionedRootHash) let highlightCenter: SWHighlightCenter = self.highlightCenter let highlight = try highlightCenter.collaborationHighlight(forIdentifier: identifier) let mentionEvent = SWHighlightMentionEvent(highlight: highlight, mentionedPersonIdentity: mentionedIdentity) highlightCenter.postNotice(for: mentionEvent) }
-
27:23 - Post an SWHighlightPersistenceEvent Notice
func postContentRenamedEvent(identifier: SWCollaborationIdentifier) throws { let highlightCenter: SWHighlightCenter = self.highlightCenter let highlight = try highlightCenter.collaborationHighlight(forIdentifier: identifier) let renamedEvent = SWHighlightPersistenceEvent(highlight: highlight, trigger: .renamed) highlightCenter.postNotice(for: renamedEvent) }
-