Faster UIActivityViewController Share Sheets with UIActivityItemProvider

The iOS 13 Share Sheet.
The iOS 13 Share Sheet.

One of the most common and annoying things I notice when using iOS share sheets is that often just triggering them will lock up the user interface for a short while before presenting any options.

It’s a plague. And I’m sure the blame falls to a previous misinterpretation that you need to provide UIActivityViewController a URL to a valid resource in order to get the sharing sheet to share an actual file. And if you just try to pass UIActivityViewController some raw Data you’ll get an export similar to below. Your choices seem pretty limited.

Boo, unnamed and useless data. What even is this?

The code often goes something like this:

  1. Get a temporary place
  2. Generate the file in this place
  3. Provide UIActivityViewController the URL to the file
func showShareSheet(recipe: Recipe) {
    // Do expensive work to make the document
    let temporaryURL = URL(fileURLWithPath: NSTemporaryDirectory() + "\(recipe.name).recipe")
    let recipeDocument = RecipeDocument(fileURL: temporaryURL)
    recipeDocument.load(recipe: recipe)
    recipeDocument.save(to: temporaryURL, for: .forCreating, completionHandler: { success in 
        guard success else { return }

        // Pass URL to UIActivityViewController
        let activityViewController = UIActivityViewController(activityItems: [temporaryURL], applicationActivities: nil)
        DispatchQueue.main.async {
            self.present(activityViewController, animated: true, completion: nil)
        }
    })
}

The issue here is that this must be ran on the main thread or you must delay the showing of your share sheet until the work has been complete. This is bad:

  • The pause and delay is almost always long enough to make the user second guess their action
    • There’s also a punishment here: If the user taps twice, the dialog will dismiss after the UI becomes responsive or you must guard against this edge case if you are doing work in the background. At this point, the mechanisms for working around an unresponsive UI begin scaling.
  • Resources are being used for computational work before being asked of it
  • Dismissing this window without action discards the work, which went untouched during its entire lifecycle and you can’t get it back without explicit effort.

Think about it – showing what you can do with a document is explicitly different than doing something with that document. The user, subliminally or not, knows and understands this. This UI pattern is nearly fundamental and they will feel the divergence.

Making matters worse, even doing something as simple as serializing a simple JSON file to the temporary storage location results in a pause that is noticeable in my testing. And more often than not, compiling information to form a document representation of what your user is editing requires a little more work than that.

But there’s hope : I’m here to show you otherwise and how to speed up your UI substantially using UIActivityItemProvider.

 

UIActivityItemProvider

UIActivityItemProvider acts as a proxy for the data, asking for its construction when absolutely necessary, all while doing this work off the main thread! Using this to our advantage, we can simply do work after we provide a path to where the resource will exist.

The Document URL Wrapper

The first step involves subclassing UIActivityItemProvider:

class RecipeDocumentURL: UIActivityItemProvider {

    let temporaryURL: URL
    let recipe: Recipe

    init(recipe: Recipe) {
        self.recipe = recipe
        self.temporaryURL = URL(fileURLWithPath: NSTemporaryDirectory() + "\(recipe.name).recipe")
        super.init(placeholderItem: temporaryURL)
    }

}

There’s a few things to note in the example above:

  • We are creating a temporary URL, adding our file name, and then passing that to UIActivityItemProvider‘s init handler. This let’s the ItemProvider know we are returning a URL.
    • Note: The resource does not exist, yet. However, UIActivityViewController now knows you are providing a URL with type extension (.recipe) and can look up UTI information to share among Action and Share extensions (and even provide a proper file representation preview).
  • Our initializer accepts and retains the data required in order to construct our document.

The Document Generation

Next we override the items() method. UIActivityItemProvider will call this after a user selects an activity from the share dialog and this is the method that will be called off the main thread to prevent the UI from locking up. The meat of the file construction operations will go here:

override var item: Any {
    get {
        let recipeDocument = RecipeDocument(fileURL: temporaryURL)
        recipeDocument.load(recipe: recipe)
            
        let data = try? recipeDocument.contents(forType: "com.recipe")
        try? recipeDocument.writeContents(data, andAttributes: nil, safelyTo: temporaryURL, for: .forCreating)

        return temporaryURL
    }
}

This is a simple example, but I’ll go through the steps:

  • We create our document. Here I’m using a UIDocument subclass, passing the URL, then loading the model representation into it with load(recipe:). In your case, this is where the file generation operations would happen.
    • Note that I save the document synchronously to ensure it exists for the next step
  • I provide the URL for UIActivityItemProvider to hand off. At this point, it points to an existing and valid resource.

And that’s it! So simple

 

iOS 13 shared file – Caramel Recipe
The joke is on you, user! This file doesn’t exist, yet!

Here is a rough skeleton of the class that I use to wrap Recipes for documents sharing:

class ShareViewController : UIViewController {
    func shareSheet(recipe: Recipe) {
        // Make Document
        let recipeDocument = RecipeDocumentURL(recipe: recipe)
        let activityViewController = UIActivityViewController(activityItems: [recipeDocument], applicationActivities: nil)
        self.present(activityViewController, animated: true, completion: nil)
    }
}

class RecipeDocumentURL: UIActivityItemProvider {

    let temporaryURL: URL
    let recipe: Recipe

    // Provide URL and gather required resources
    init(recipe: Recipe) {
        self.recipe = recipe
        self.temporaryURL = URL(fileURLWithPath: NSTemporaryDirectory() + "\(recipe.name).recipe")
        super.init(placeholderItem: temporaryURL)
    }

    // Make file and provide our URL
    override var item: Any {
        get {
            let recipeDocument = RecipeDocument(fileURL: temporaryURL)
            recipeDocument.load(recipe: recipe)
            
            let data = try? recipeDocument.contents(forType: "com.recipe")
            try? recipeDocument.writeContents(data, andAttributes: nil, safelyTo: temporaryURL, for: .forCreating)

            return temporaryURL
        }
    }
}

 

These APIs have been available since iOS 10, but a lot of documentation surrounding them is vague or plain outdated. Now that this information is out there, let’s make our solemn pledge to make Activity Sharing Sheets better together!

 

There’s still a little bad news… 

The caveat here is that it is on Apple to provide some sort of mechanism for letting the user know an operation is taking place and currently in iOS 13.1 they have nothing. At a previous point, the action icon used to dim with an activity animation overlaid during the work process, but seems to no longer be the case. Until then, there’s not much to note something is happening but your UI does remain responsive during this period, shifting the burden onto the bear. 💁🏽‍♂️

I am Dan Griffin and you can find me on Mastodon
iOS

The Blog

Base & Elevated System (and Grouped!) Background Colors

In iOS 13, Apple introduced a slew of new colors that are also dynamic – meaning they will adjust between light and dark modes (and other scenarios, such as high contrast). Of the new colors, the various background colors are pretty pecular: iOS defines two sets of background colors—system and grouped—each of which contains primary,…

iOS iOS 13

Always Taking Inquiries

At the moment I am not taking on many new projects, but am still available for inquiry or questions.

Reach Out To Dan