Preface
Apple introduced a new protocol called Transferable
in the Core Transferable framework to help simplify the code to implement sharing between apps. It works deeply with SwiftUI ShareLink
and other framework protocols like Codable
. Just like SwiftUI, it has a declarative-based API design and is relatively easy to get your hands on.
However, its API design may not seem straightforward, and there are some pitfalls while using it. This article will discuss how to use the API and one particular pitfall I encountered on iOS 17, as well as the solution to it.
If you are familiar with the usage of this protocol, you can just skip the next part and jump to the pitfall & solution part.
Basic usage on sharing
Traditionally, when implementing a sharing feature, like sharing a file via the system’s share sheet, we need to implement in those steps:
Place a button in the user interface.
As the user click the button, prepare the file to be shared.
Then call a method to invoke the sharing sheet like
UIActivityViewController
.
The system handles the left for you.
The procedure above is a imperative way to do sharing. Using the Core Transferable + SwiftUI, however, you will do this way:
Provide a sharable content by adopting the
Transferable
protocol.
Implement this protocol by providing at least one
TransferRepresentation
. Here you should determine what the representation of your sharing content will be, for example, as a file or a data or simply a string.
Declare a ShareLink SwiftUI view bound with the sharing content you provided in the view. Then when the user click the view, the share sheet will be automatically displayed and when the user selects the sharing destination, one of your
TransferRepresentation
instances will be used to provide data to share.
Do you see the difference between the imperative and the declarative way? The declarative way is to let us define how the data is represented while sharing, and the system does the rest. We don’t need to worry about how to communicate with the UIActivityViewController
and avoiding the crash when showing UIActivityViewController
on iPad without configuring the properties of popoverPresentationController
.
Let’s see the code in action.
Implement your Transferable data
Consider an Article structure that includes a title and a link. We define this structure as follows:
Then before adopting the Transferable
protocol, we should think about how the representation this object will be when sharing. OK, here is the one: we put the title and the url in a string. To implement this, we added a compute property of the structure:
Then implement the Transferable
protocol:
Here we use ProxyRepresentation
instance to refer to the sharingText
, which is a String type and the String type already adopted the Transferable protocol by the system.
Use SwiftUI view to display share sheet
Once we have prepared the data and adopted the Transferable
protocol for sharing, we can pass it to the ShareLink
view and specify the label to be displayed on the button.
That's quite easy, right? However, you might ask: what if I don't have the URL while sharing the article? For instance, I might need to perform a network request to obtain the URL to share. In real scenarios, additional URL queries may be appended to help track the sharing source. In this case, you may wonder how to perform the network request or other time-consuming tasks while preparing the sharing data in advance.
Advanced: encapsulate the data preparation inside Representation
Let’s say we need to perform a network request before sharing:
You might remember that we can’t control how the share sheet is presented when the user click the ShareLink button, since the Transferable
protocol should encapsulate all the logic to prepare the content to be shared. In other words, we can’t decide when to call the getURLToShare
method since we can’t know the exact moment when the user clicks the button.
You might also consider calling getURLToShare
before the user clicks the button, such as when the user begins viewing the article. This way, you can control whether the button is enabled by checking if getURLToShare
has been called and has returned the correct result. However, this approach will require additional resources, as users may not always want to share the article.
Actually, when you define your own Representation of your content, there are some methods that run asynchronously.
We can invoke the getURLToShare
method within the block and return the URL for sharing. Additionally, the block is capable of throwing errors. If an error occurs, the sharing process will be aborted.
Note that in the
getURLToShare
implementation, we should handle the case where the Task will be cancelled. The block will be invoked multiply times in the progress of sharing. So be prepared to handle task cancellation error and release necessary resources.Also, the example above uses a deprecated method since iOS 17. Apple recommends that the synchronous exporter should be used instead. However, some other
TransferRepresentation
implements likeFileRepresentation
still have asynchronous exporter method to use.
FileRepresentation issue on iOS 17
So far we have learned how to implement the sharing feature using Transferable protocol combined with SwiftUI. There is a pitfall and a undocumented change on iOS 17, which will cause failure when sharing internal files to the system Files app on iOS.
Let’s say we would like to define a Transferable
to share an internal file:
It takes a URL as a parameter and return it in the FileRepresentation
block.
The internal URL may be created in this way for testing purpose:
And also we use the ShareLink view to display the UI:
This code will work well on iOS 16 and other share destinations except the “Save to Files” on iOS 17. On iOS 17, the Files app will be launched with empty view and dismiss after a while, failing to receive the file we share.
If we check the Xcode console, we can only see the following warning:
cancelled request - error: The operation couldn’t be completed. Invalid argument
Well, what could possible wrong? Because the code is quite simple and we only pass a URL to share. So let’s do some basic troubleshooting.
Basic Troubleshooting
The warning from Xcode is vague and it can’t show the outer processes’ logs. Here, we can use the Console app on macOS to view other processes’ logs.
By filtering and finding the log, we can see what had happened in the SaveToFiles process:
It seems like it can’t read the file as it says no such file or directory. To make sure the file exists, we can download the app container to the Mac and view its contents.
Once downloaded, we can view its content by selecting “Show Package Contents” button.
And we can find the file we just created and verify that the content we write into is correct:
So what had happened? It’s clear that the file exists and it seems that the Files app or SaveToFiles process can’t have permission to read it.
Temp solution: Set UISupportsDocumentBrowser
This is a brutal solution to solve this issue.
Since it may be due to permission issue, why don’t we just make sure the app is always accessible? On iOS, we can allow other apps to open and edit the files stored in our app’s Documents folder. By setting the UISupportsDocumentBrowser
and LSSupportsOpeningDocumentsInPlace
in the info.plist, you can allow the app’s Documents folder to be displayed in the Files app and other apps or users can be able to open and edit files in it.
It’s not a perfect solution since you are exposing the whole Documents to other apps and users and if you are not building a real document-based app, this solution should not be adopted.
Solution: Provides a ProxyRepresentation
I discovered this solution by the WWDC video that introduces the Transferable protocol. Although not entirely relevant, the video session said we can provide a ProxyRepresentation to return the URL itself following the FileRepresentation in case that the receiver doesn’t accept the file itself but the URL object.
Thus, after some attempts, you can fix this issue by doing this:
There are some noteworthy reminders:
The order of
FileRepresentation
andProxyRepresentation
is important.FileRepresentation
takes higher priority, and if it is not suitable for the destination,ProxyRepresentation
will be used instead.
On iOS 16, if we use just the
ProxyRepresentation
returning the URL object, it’s still treated as an URL object, which means that if we choose to Save To Files, a txt file with the content of URL will be saved instead. So to make this compatible on both iOS 16 and iOS 17, we need to provide those twoTransferRepresentation
.
Here is the solution to this issue. However, the underlying cause remains a mystery, and there are few clues available on the internet. I have searched and found the following message regarding the changes introduced in iOS 17:
https://developer.apple.com/forums/thread/733642
If you discover any relevant or significant changes introduced in iOS 17 causing this issue, please feel free to leave a comment in the section below.
I've been debugging this issue for a while before finding this post. Using the ProxyRepresentation solution works some times, but not always. In my case it will not work if I try to share video content from my app with other apps like Instagram.
I'm starting to think this might be a defect in the SwiftUI ShareLink implementation, because the same URL that fails with ShareLink, will work with the old UIActivityViewController, without having to add values to the plist file and opening up the documents folder for the whole world.