Share arbitrary struct from HostApp to Extension and get arbitrary result back

I want to share a Transferable (JSON-encoded) data struct from some HostApp with an Extension of my ContainingApp, and get a different Transferable (also JSON-encoded) data struct back on success.

Since I want to present my ContainingApp's AppIcon in the sharing sheet to make it easy for the user to find it, I started building a Sharing Extension and not an Action Extension. AFAIK the difference is only in the presentation (Sharing Extension: Icon+name of the ContainingApp vs Action Extension: simple b/w system icon plus a string describing the action (e.g. "Copy", "Save to Files")), and the data flow is identical. Please correct me if I'm wrong.

I added the Sharing Extension to my ContainingApp (which are both in the same app group so they can use a shared container to exchange data). The (real) HostApp is from a different company we are collaborating with, and thus is not in our app group. Once everything runs I will add a tailored NSExtensionActivationRule to make sure our Sharing Extension is only shown to our partner's HostApp. Currently I am still using TRUEPREDICATE. The goal is that after the user tapped the "Continue with ContainingApp" (Share-)button in the HostApp, iOS will only show my ContainingApp icon and nothing else, since that's the only useful choice for the user.

Side Question 1: The best user experience would be if the HostApp could directly present our extension when the user tapped the "Continue with ContainingApp"-button, without the user needing to choose it manually in the Share-sheet, but I guess this is not possible for privacy/security reasons, right?

In the debugger of the HostApp I see this error:

	Type "com.myapp.shareInput" was expected to be exported in the Info.plist of Host.app, but it was imported instead.
		Library: UniformTypeIdentifiers | Subsystem: com.apple.runtime-issues | Category: Type Declaration Issues

but I definitely want to define and export both ShareInput and ShareResult as UTExportedTypeDeclarations in my extension, and all 3rd-party apps (like this demo HostApp) using my extension need to import them.

Side Question 2: Can I just ignore this error? And tell the 3rd-party app developers they also can ignore it?

After the user tapped on the ContainingApp icon in the sharing dialog, my Sharing Extension will show its dialog, presenting the shared item it got from the HostApp, and let the user edit the text.

When the user taps the "Save"-button in my extension, it creates a ShareResult struct to send back to the HostApp, and dismisses the sheet.

This (kinda) works when I share plain text with the 􀈂Text button in my HostApp. My ContainingApp icon is shown together with Mail, Messages, and other apps that can process plain text; with shortcuts to persons and devices (AirDrop targets) in the line above, and with actions (Copy, New Quick Note, Save to Files, Save to Citator, Certificat, Airdrop) below.

When I choose my ContainingApp, the extension runs and shows the text it got. ("Kinda" because I am still struggling to send data back. See below...) So the principal operation works...

Side Question 3: In the HostApp, can I use ShareLink() to present the Share-sheet and receive the result struct or do I always need to

        activityViewController!.completionWithItemsHandler = completionHandler
		windowScene.keyWindow?.rootViewController?.present(activityViewController!, animated: true, completion: nil)

and process the result in the completionHandler?

If returning (any) data from the extension is possible with ShareLink() also, then how? I didn't find any sample showing this...

I implemented the ShareLink() anyway (and ignore the result part for the moment).

When I try to share a ShareInput struct with the 􀈂ShareLink button, the same persons are sorted differently, there are less app icons (9 instead of 13), and less actions (only 3: New Quick Note, Save to Files, AirDrop): Note that while the preview correctly shows the preview text provided ("shareInput"), the preview image left of it is blank (instead of arrowshape.right.fill):

	        let preview = SharePreview("shareInput", image: Image(systemName: "arrowshape.right.fill"))

When I choose my ContainingApp, the extension runs ...

On iOS17, I see that indeed my ShareInput data arrived in my extension:

	❗️itemProvider=<NSItemProvider: 0x301b1c460> {types = (
	    "com.myapp.shareInput"
	)}
		Library: ShareExtension | Subsystem: com.myapp.containingdemo.ShareExtensionDemo | Category: ShareSheet

However, on iOS 16 it doesn't work:

	Host[8615:634470] [Type Declaration Issues] Type "com.myapp.shareInput" was expected to be exported in the Info.plist of Host.app, but it was imported instead.
	Host[8615:634462] [ShareSheet] Couldn't load file URL for Collaboration Item Provider:<NSItemProvider: 0x280f49180> {types = (
    		"com.myapp.shareInput"
	)} : (null)

That error is shown before I choose the ContainingApp to share with. When I do that, I get:

	ShareExtension[8774:636786] [ShareSheet] ❗️itemProvider=<NSItemProvider: 0x28243a300> {types = (
	    "dyn.age8u",
	    "public.file-url"
	)}

which clearly shows the ShareInput struct was not transferred to the extension.

But since I don't know how to transfer the ShareResult back to the HostApp when using a ShareLink, I cannot continue this approach anyway.

When I try to share a ShareInput struct with the 􀈂JSON button (using present(activityViewController)), I see (both on iOS 16 and iOS 17):

My extension (rather, the ContainingApp's icon) is not shown as Sharing target (even though it still has TRUEPREDICATE), which means that my code didn't manage to pack the ShareInput struct for the activityViewController - and thus it doesn't know what to share.

I did the same as with the plainText item before:

        let shareInput = ShareInput(inputStr: "ShareInput as JSON")
        let preview = SharePreview("shareInput", image: Image(systemName: "arrowshape.right.fill"))
        VStack(spacing: 25.0) {
            Text("HostApp!")
            ShareButton(title: "Text", shareItems: [ItemSource(dataToShare: "sharing some text")])
            ShareButton(title: "JSON", shareItems: [ItemSource(dataToShare: shareInput)])
            ShareLink("ShareLink", item: shareInput, preview: preview)
        }

(I will continue in the next posting)

When I share both JSON and plain text:

            ShareButton(title: "JSON", shareItems: [ItemSource(dataToShare: shareInput)
                                                   ,ItemSource(dataToShare: "sharing some text")
                                                   ])

I get (iOS 16 vs. iOS 17):

where iOS 16

	ShareExtension[9236:652213] [ShareSheet] ❗️itemProvider=<NSItemProvider: 0x280afa1b0> {types = (
	    "public.plain-text"
	)}

and iOS 17 transfer:

	❗️itemProvider=<NSItemProvider: 0x300f99e30> {types = (
	    "public.plain-text"
	)}
		Library: ShareExtension | Subsystem: com.myapp.containingdemo.ShareExtensionDemo | Category: ShareSheet

So again it's clear that the ShareInput is not transferred, but only the text.

For the result, I didn't manage to pass a ShareResult struct to NSItemProvider:

I just found

        NSItemProvider(item: contactData as NSSecureCoding, typeIdentifier: UTType.vCard.identifier)

which probably works because contactData is known to iOS...

See this thread: https://forums.developer.apple.com/forums/thread/24368

Again, transferring plain text was possible...

Main Question: How can I transfer my ShareInput struct from the HostApp to the extension (iOS 16 and later), and then my ShareResult struct from the extension back to the HostApp (iOS 16 and later)?

And how can the HostApp limit the possible sharing targets? I want neither persons nor actions to appear in the sharing dialog, just apps - preferably only my ContainingApp. This should be possible with a strict NSExtensionActivationRule, right?

(Bummer, it is not possible to upload the zipped demo project. Thus I'll need to upload to github...)

Here is the github project: https://github.com/Fesh-com/Sharing-Extension

Sorry for the delayed response @marcvienna . I was looking into your questions:

In the debugger of the HostApp I see this error: Type "com.myapp.shareInput" was expected to be exported in the Info.plist of Host.app, but it was imported instead. Library: UniformTypeIdentifiers | Subsystem: com.apple.runtime-issues | Category: Type Declaration Issues

You're seeing the error because "com.fesh.shareInput" wasn't added in the app's info.plist, keep in mind that If both imported and exported declarations for a uniform type identifier exist, the exported declaration takes precedence over the imported declaration.

To add the exported and imported type declarations in the app’s Info.plist:

  • In Xcode’s project editor, choose the project or target, then click Info.
  • In the editor area, expand the Exported Type Identifiers then click the Add button (+).

Side Question 3: In the HostApp, can I use ShareLink() to present the Share-sheet and receive the result struct or do I always need to activityViewController!.completionWithItemsHandler = completionHandler >windowScene.keyWindow?.rootViewController?.present(activityViewController!, animated: true, completion: nil) and process the result in the completionHandler?

You could use ShareLink to present the share interface. For the imported content type, Transferable. handles the CodableRepresentation handles the conversion of the model type to and from binary data. However in some cases you would need to implement the closure that instantiates the item inother to convert the binary data to a type that your app is aware of For example, A file representation for transferring the profile's video:

        FileRepresentation(contentType: .mpeg4Movie) { profile in
            guard let videoURL = profile.video else {
                throw TransferError.exportFailed
            }
            return SentTransferredFile(videoURL)
        } importing: { received in
            let destination = try Profile.copyVideoFile(source: received.file)
            return Profile(video: destination)
        }

When you define a uniform type identifier, remember you need to declare it to be either an imported or exported type:

  • An exported type declaration is declared and owned by your application. For example, the Codable schema of ShareInput is an exported content type com.fesh.shareInput. Your application is the source of truth about given type, and it is also the primary handler of files with the corresponding file extension.

  • An imported type declaration is owned by some other app, that may not be present on the device when your app is installed.

For example, suppose an image editing app creates files using a proprietary format whose uniform type identifier is declared in its application bundle. If you are writing an application or plugin that would read such files, you must make sure that the system knows about the proprietary uniform type identifier, even if the actual image editing application is not available. To do so, your application would declare the uniform type identifier in its Info.plist as imported.

And how can the HostApp limit the possible sharing targets? I want neither persons nor actions to appear in the sharing dialog, just apps - preferably only my ContainingApp. This should be possible with a strict NSExtensionActivationRule, right?

Correct, NSExtensionActivationRule controls the system filtering the available share extensions the user can choose based on knowing that the share extension has declared support for some Type Identifier in the payload. I suspect the issue you might be facing is that the type identifier isn't granular enough. For example, json UTType also conforms to UTTypeText which represents text with markup.

Share arbitrary struct from HostApp to Extension and get arbitrary result back
 
 
Q