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
I want a Press Kit

When 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.

Share sheet SwitchBuddy example

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!

Bluesky logo

Follow on Bluesky to not miss new posts

Filip Němeček profile photo

WRITTEN BY

Filip Němeček Mastodon

iOS blogger and developer with interest in Python/Django.

iOS blogger and developer with interest in Python/Django.