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.
The code often goes something like this:
- Get a temporary place
- Generate the file in this place
- 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
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. 💁🏽♂️