Sharing data with UIActivityViewController - tips & tricks
All the tips and tricks I have discovered so far. Better previews, subtitles. PNG via AirDrop & more.
Published: Feb. 25, 2023 Sponsored I want a Press KitWhen you want to share data in iOS via the “share sheet" the basic workflow can be to create UIActivityViewController
, and pass it some data like URL
, UIImage
and similar, and it will work.
Code like this:
let shareVC = UIActivityViewController(activityItems: [url], applicationActivities: nil)
present(shareVC, animated: true)
However, the experience is only sometimes great for the user; for example, with images, the system controller leaves an empty header that looks weird.
In SwitchBuddy, there is quite a lot that can be shared, anything from links, image data, or local URLs containing files or videos, and over time I tried to improve the experience.
I found that it practically always makes sense to spend some time creating a type that conforms to UIActivityItemSource
. This will let you control the share sheet in many more ways than expected.
Since the UIActivityItemSource
requires a subclass of NSObject
, you cannot use struct
. While you can conform existing classes, I started creating types to be used in the share sheet to better separate logic and keep my other data primarily as structs.
The basics of UIActivityItemSource
The UIActivityItemSource has two required methods:
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return title
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return image
}
The placeholder - this is sometimes used when loading the actual share data takes time and the itemForActivityType
is where you return the data that get shared.
And yes, you can return different data there, but let's not get ahead of ourselves.
If you implement just the above, you get the same behavior as adding the data to UIActivityViewController
.
Creating better previews
For creating better previews for the share sheet header, there is an optional method func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata?
.
This uses the LPLinkMetadata
from the Link Presentation framework. For URLs, the system can fetch the metadata even without you using the UIActivityItemSource
but you don’t get any control over this.
When you create the metadata by hand, you can customize the displayed icon and title. There are more attributes to use, but in my usage, adding just the title
and iconProvider
dramatically improved the share sheet experience.
The subtitle trick
Did you notice some apps or contexts displaying subtitles under the title? There is no subtitle
property on the LPLinkMetadata
object to use for this. But you can use the originalURL
property to “fake” the subtitle.
And while this expects a URL, you can use this init URL(fileURLWithPath:
to create it with any string you want.
With all that said, here is an actual example from SwitchBuddy:
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
let metadata = LPLinkMetadata()
metadata.iconProvider = NSItemProvider(object: UIImage(named: "Logo")!)
metadata.title = title
if let subtitle = subtitle {
metadata.originalURL = URL(fileURLWithPath: subtitle)
}
return metadata
}
This is used when the user shares a large image so I think it makes sense to show the app icon instead of the squished image preview.
The method above is part of a special class I have just to be able to share images with previews, and it looks like this:
import UIKit
import LinkPresentation
final class ShareableImage: NSObject, UIActivityItemSource {
private let image: UIImage
private let title: String
private let subtitle: String?
init(image: UIImage, title: String, subtitle: String? = nil) {
self.image = image
self.title = title
self.subtitle = subtitle
super.init()
}
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
return title
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return image
}
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
let metadata = LPLinkMetadata()
metadata.iconProvider = NSItemProvider(object: UIImage(named: "Logo")!)
metadata.title = title
if let subtitle = subtitle {
metadata.originalURL = URL(fileURLWithPath: subtitle)
}
return metadata
}
}
And then I can use it like this:
let shareImage = ShareableImage(image: image, title: gameName, subtitle: "Game screenshot")
let shareVC = UIActivityViewController(activityItems: [shareable], applicationActivities: nil)
present(shareVC, animated: true)
Customizing share data based on the destination
Another use for the UIActivityItemSource
is to change what is shared based on the action that the user selects.
In SwitchBuddy, users can share news articles about Nintendo and similar topics. My first implementation was just to create a string
containing the article's title, the source website, my app hashtag and finally the URL. This approach worked great when shared via Messages and similar, where the inline URL would get parsed, and the app would display the rich preview.
One day I wanted to AirDrop an article to my Mac to read it there and discovered that it got saved as a .txt file in my Downloads folder instead of opening in a browser.
Thankfully this is easily solved by a method we already met.
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return shareText
}
We can inspect the activityType
parameter to check the destination for our data and return something else.
So in my case, the implementation looks like this:
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
if activityType == .airDrop || activityType == .addToReadingList {
return url
} else {
return shareText
}
}
If the action is AirDrop or saving to Safari Reading List, I am returning just the URL and, otherwise the text with more information.
Specifying type of shared data
When you share Data
or UIImage
objects, specifying the actual file type is a good practice. For this, we can use the following method:
func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivity.ActivityType?) -> String {
}
And since iOS 14, we have available the UTType
struct that replaces the previous global constants.
So to specify we are sharing PNG data, we would do something like this:
func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivity.ActivityType?) -> String {
return UTType.png.identifier
}
Keeping PNG when sharing via AirDrop
Another potential stumbling block I discovered is that AirDrop seems to “prefer” sending images as JPEG even when we specify the type as png. So apparently, the fix is to check the activityType
and use pngData()
method on the UIImage
for the AirDrop like this:
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
if activityType == .airDrop {
return image.pngData()
} else {
return image
}
}
And that's it for now!