We’ve been working on a Share Extension for the Bipsync Notes iOS app which will allow users to ‘clip’ notes into Bipsync from Mobile Safari. The new feature will be available soon, but while developing the extension we’ve learned a few handy tricks that we wish we’d learned earlier, which we thought we’d share.
Apple allows us to use JavaScript to inspect and even manipulate the contents of a web page before it’s then shared with an arbitrary service, but information on this topic seems to be in short supply. So if you’re developing a Share or Action iOS extension to work with HTML in a web page, the following may be of interest to you…
1. Debug your code using Safari’s Developer tools
By providing a key called “NSExtensionJavaScriptPreprocessingFile” in your extension’s Info.plist we can tell iOS that we want to pre-process the contents of the webpage being shared by providing the name of a JavaScript file that should be executed when the extension runs.
This is vital for an extension like ours, which seeks to extract the pertinent content – we can determine which elements of the page are relevant to the main topic (i.e. the “article”) and strip out less relevant data such as footers, adverts, menus and so on, while working in the context of the page itself with access to its DOM.
Trying to parse HTML content is much easier when we’ve a DOM to work with. I dread to think how complicated it would be to do the equivalent processing in Objective-C or Swift with regular expressions or some sort of DOM/XML parser.
A feature like this requires a substantial amount of development, and as with all development the ability to inspect and debug what’s happening when our code runs is crucial.
Since the JavaScript code is being executed in a JavaScript context which exists outside our extension, we can’t do this through Xcode’s debugger. It’s not obvious, but we can use Safari’s Web Inspector on the host machine (i.e. not the iOS device, but the Mac OS device that’s running Xcode) to attach a debugger to our pre-processing code and receive error message, do step-through debugging, and so on.
First we enable the develop menu by checking the appropriate box at the bottom of Safari’s “Advanced” menu, on our Mac OS machine. Then we launch the clipper extension from Xcode and tell Xcode to open it with Mobile Safari.
Once the page has loaded we go back to Safari on the Mac OS machine and from the new “Develop” menu that appears in the menu bar we come down to the appropriate device (e.g. Simulator if you’re working with the iOS simulator or the name of your device if you’re developing on real hardware) and from the nested submenu we choose the entry which matches the page that we’re viewing in Mobile Safari:
Now we’ve attached an instance of Web Inspector to the web page we’re about to interact with in our extension.
If we open the share menu in Mobile Safari and then tap on our extension, iOS will execute our JS pre-processing file in the context of the web page. This means that we can attach debuggers (either through placing manual breakpoints or typing debugger; in our code; see error messages and thrown exceptions; and interrogate values of variables and the like through the Console. This is a much better approach than making changes to the pre-processing file in a trial and error fashion until it works without error.
2. Manage App Transport Security
If your extension makes HTTP requests to an external service and you find yourself wondering, as I did, why they seem to be disappearing into the ether, never arriving at your server – check your App Transport Security settings. Since iOS 9 any requests that aren’t made over HTTPS have to be greenlit by the developer.
The problem we had was twofold: while developing the extension we were talking to a local server over HTTP, and our existing ATS settings were limited to the iOS app’s target not our new extension. So by adding another entry to the extension’s Info.plist file we were able to whitelist our local development server, and begin receiving requests from the simulator.
In the example above we’ve made an exception for anything running on the *.bipsync domain (our local development environment often runs on dev.bipsync, for example) so iOS will allow requests to be made over HTTP.
3. Keep your JavaScript code maintainable
Should your JavaScript pre-processing code become substantial (ours is currently ~2.5k lines long) you’ll find that keeping it all in one file is awkward, to say the least. It makes reading the code and navigating through it difficult, and the obvious solution is to break the content up into a few files, usually employing a rule such as one file per Class or some other logical grouping.
This is difficult since iOS only knows about the one file that you specify in your extension’s Info.plist as being your pre-processing file (NSExtensionJavaScriptPreprocessingFile).
Since we aren’t developing a web app with access to the web page, there’s no way to dynamically include other scripts in your pre-processing file so they can be referenced [I suppose it might be possible to load them into the web page you’re intending to process, but that feels so wrong to me I didn’t explore the possibility].
The solution we arrived at was to provide a very simple pre-processing file which just contains placeholders to the other files we’re including:
The file above will eventually contain the contents of three files, loaded in order of dependency. The bottom-most one, “main.js”, contains the ExtensionPreprocessingJS variable that Apple expects the extension to provide. This variable will be called at runtime when the user invokes your extension. We also include Readability, a library from Mozilla that is able to parse a webpage to determine pertinent content, and a class of our own called “Clipper” which performs further processing on the resultant HTML to get it into a format that Bipsync can work with.
To actually stitch these files together into compiled.js we have a Build Phase script which runs a Fastlane command when the app is built (writing Ruby scripts in Fastlane has proved an easier way to manage these ad hoc scripts than writing nasty Bash scripts into Xcode’s tiny textfields). That command looks like this:
It’s simple and won’t work for really complicated dependencies, but it’s a more manageable approach than trying to work with a single humongous file.
So there you have it. Those are the three simple tips we wish we’d known when we started writing our Share Extension. We hope they’ve helped you write yours!