What we learned building the Tumblr iOS share extension
· 8 minute read
iOS app extensions – launching this Wednesday, as part of iOS 8 – provide an exciting opportunity for developers of all types of apps to integrate with their customers’ devices like never before. Here at Tumblr, we’re thrilled to pull the curtain off of our share extension, which we’ve been working hard on for quite a while now.
The process of building the Tumblr share extension has been fun, but also really frustrating at times. We’ve hit quite a few problems that we ended up needing to work around, and in the interest of helping you do the same, would like to detail all of the issues that we encountered.
Of course, your mileage may vary with some or all of these. We’ve talked to other developers who haven’t had the same problems, or have hit some that we haven’t. To make it easy to track updates to these problems and share workarounds, we’ve created an issue in this GitHub repo for each one. Please create pull requests if you’ve got solutions, or issues if you’ve encountered something that we didn’t.
We couldn’t get background file uploads to work
Apple’s App Extension Programming Guide contains a section on performing uploads and downloads, complete with sample code indicating how background sessions are to be used to perform uploads that may last longer than your extension process is alive for. Normally, an
NSURLSessionUploadTask can be created from a stream, raw data, or a file URL, but only the latter is intended to be used in an extension. This makes sense: communication between extensions and container applications in the same “app group” occurs through a shared container, a special, secure location on disk that both extension and app are able to read and write from. The extension writes a file to the shared container and initiates a task to upload that file. The upload ostensibly occurs in a third process, allowing it to continue even once the extension has been terminated. The container application will then later be woken up and notified as to its success or failure.
We have not been able to get this to actually work.
In our experience, while our extension and container application can both access the shared container without issue, the
NSURLSessionTask is seemingly unable to. Instead, it spits out errors that you can find in the radar.
As soon as a user taps the “Post” button, we’d ideally like to dismiss the extension and let them get on with their day, while continuing to upload in the background. Given that we haven’t been able to get this to work, we’ve given our extension a progress bar and are keeping it on screen until the request completes. It’s possible that the user could background the host application, and iOS could kill it in order to reclaim the memory, but this seems like our best option given these limitations. We’ll happily go back to using background sessions if the issue we’re seeing ends up getting fixed.
The container application must be opened before the share extension can be used
As mentioned, the shared container is where everything that you need to access from both your app and extension must be located: user defaults, keychains, databases, files that you’re serializing via
For existing apps, the problem is simple; the data already exists somewhere outside of the shared container, and only the container app can migrate it over. Thus, if the user installs an update that adds an extension, and tries to use the extension before launching the application and giving it a chance to perform the migration, they’re going to have a bad time.
There’s no great option here. If the user opens our extension first, we just throw up a dialog telling them that they need to launch the application first. Inelegant but necessary.
We couldn’t get
NSFileCoordinator to work
NSUserDefaults and SQLite are useful for synchronizing data access across both extension and container application, but as per WWDC Session 217,
NSFileCoordinator is also supposed to be an option for those of us using
NSCoding for custom data persistence. We tried hard, but couldn’t actually get it to reliably work.
Our use case required both our app and extension to write to the same file, where only the app would read from it. We observed a number of problems while both extension and app processes were running simultaneously.
NSFilePresenter methods intended to indicate that the file had been or will be modified (
relinquishPresentedItemToWriter:) would either:
- Not be called at all
- Only be called when switching between applications
- Be called, but only after a method that would cause the app to overwrite the data that the extension had just written (either
relinquishPresentedItemToReader:) was called first
Rather than trying to keep access to a single file synchronized across processes, we modified our extension to instead atomically write individual files, which are never modified, into a directory that the application reads from.
This isn’t to say that
NSFileCoordinator isn’t currently a viable option if you’ve got a different usage than we do. The New York Times app, for example, is successfully using
NSFileCoordinator in a simpler setup, where the container app is write-only and the extension is read-only.
Share extensions can’t set the status bar color
The Tumblr share extension – like its container application – has a dark blue background color. White looks great on dark blue. Black, not so much.
We tried everything, but couldn’t find a way for our share extension (which uses a custom view controller subclass, as opposed to
SLComposeServiceViewController) to specify its status bar style. Instead, we always get the status bar style of the host application. Since we’re expecting Photos.app and Safari – both which have black status bars – to be two of the apps that Tumblr users share from the most, this is really disappointing.
None so far. Neither Info.plist keys nor view controller methods worked, and we couldn’t even get a handle to the keyboard window the way that applications can usually accomplish using private API (Sam Giddins nearly went insane trying. Thanks Sam!). Here’s hoping for a way to do this in iOS 8.1.
You can’t exclude your own share extension from your application’s activity controllers
It makes sense that you can’t specifically exclude a specific share extension from an activity view controller. We wouldn’t want Instagram doing something like preventing sharing to Twitter, would we?
But the one extension that you should be able to remove from your own app’s activity view controllers is your own extension. It’s silly to be able to share to Tumblr from within Tumblr. I mean, it works. It’s OK, I guess. But it’s weird.
None so far. We tried configuring our activity controllers with an activity item with a custom UTI, and then specifically giving our share extension a predicate that would cause it to not show up when said UTI was present, but it had unintended side effects, which brings us to the next issue…
By default, share extensions will only show up if they explicitly support all of the provided activity items
Radar #18342403: NSExtensionActivationRules should only need to match a single activity item for a share extension to be displayed Radar #18150467: Documentation for custom NSExtensionItemActivation rules is very vague
This is a doozy. It’s the most important issue we’ve found, and one that probably deserves a blog post of its own.
Here’s how applications pass data to share extensions:
- An application configures a
UIActivityViewControllerwith an array of “activity items”
- The activity controller displays the system activities and share extensions that can operate on the types of items provided
Here’s how we think this should work, using the Tumblr app as an example:
- The user long-presses on a photo
- We put the image data, the posts’s URL, and maybe a text summary of the post, all in the activity items array
- We’d expect share extensions that support either image data or URLs or text to all show up in the activity controller
What actually happens is that only share extensions that explicitly support images and URLs and text will show up.
This is a problem, because the simplest way to specify what your extension supports – and by far the best documented – is by adding
NSExtensionActivationRule keys like:
`NSExtensionActivationSupportsText` : `YES`
This looks like it would mean “show my extension as long as any of the activity items are text,” but it really means “show my extension as long as there is only one activity item, and it is text.”
Federico Viticci, who at this point has likely used more third-party share extensions than anyone else on the planet, verifies that this is in fact a legitimate problem:
@irace Yup. Been talking to devs to handle exceptions when possible, but I'm getting a lot of failures in several apps.— Federico Viticci (@viticci) September 5, 2014
@irace Yep. And, that the input passed by an app doesn't match what another app's extension expects and you get all sorts of weird stuff.— Federico Viticci (@viticci) September 5, 2014
This negatively affects both app and extension developers. It means that:
App developers should only configure their activity controllers with a single activity item. There are a couple of problems with this. First, it’s doable, but a pain if, like in Tumblr.app, you want system activities like copying and saving to the Camera Roll to support multiple different types of data. Secondly, it’s a huge shame to only export one type of data and limit the number of sharing options that your users will be able to perform.
Extension developers should use the more complex (and unfortunately, not very thoroughly documented) predicate syntax to specifically specify an OR relationship. This would look something like:
SUBQUERY(extensionItems, $extensionItem, SUBQUERY($extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image").@count = 1 OR
SUBQUERY(extensionItems, $extensionItem, SUBQUERY($extensionItem.attachments, $attachment, ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text").@count >= 1).@count >= 1
Radar #18207630: Table view content insets get adjusted wildly when rotating a share extension (sample project). Minor, especially relative to the rest of these issues, but we’re already over 2,000 words here. What’s a few more?
A huge thanks to Matt Bischoff, Paul Rehkugler, Brian Michel, and Sam Giddins for not only helping find these issues and employ these workarounds, but for filing radars, creating sample projects, and helping edit this post as well.
And of course, to the frameworks and developer evangelist teams at Apple. With extensions, you’ve given us a prime opportunity to delight our users even more. We’ve got lots more ideas and can’t wait to see what everyone else comes up with as well.