We’ve always used the same technique to load and display notes in our iOS app. This has involved using an embedded web server and a web view to host the note editor UI, which is written in HTML, JavaScript and CSS. When the user selects a note, we send a message containing the note’s data to the web view, and the view uses JavaScript to update the display.
This approach has served us well, but it has not been without its issues. That’s not a criticism of the web servers we’ve used. CocoaHTTPServer, and more recently GCDWebServer, are very high quality libraries that have worked well for us. For the majority of the time, this technique was fine – but iOS as a platform threw us a few curveballs that have ultimately forced us to adopt a different strategy.
The problems
There were several of these. Let’s discuss each in turn.
Blocked requests by MDM Solutions
Many of our customers employ Mobile Device Management (MDM) to secure their employees’ devices. Large organisations often want to ensure their devices are easy to maintain and upgrade, managed by their IT team from a remote location.
A feature of many MDM solutions allows an organisation to restrict the URLs and ports that can be used on a device. We found this to be problematic when we first encountered it, because in some cases this either prevented the web server from starting, or loading the web page which contained the editor’s code. Eventually we addressed this in two ways:
- We changed the host name at which we loaded the editor from ‘localhost’ to ‘127.0.0.1’.
- We added a reload loop which attempted to start the server on different ports until it found one that was valid and free.
After these concessions the note editor loaded reliably on MDM managed devices, but it made us wary of the effects that such a solution could have on our app.
Terminated processes
It’s important that any application which receives user input, doesn’t lose that input. For our app, that means we have to make sure that text entered into the editor, i.e. the web view, is transferred to the app’s main process where it can be saved first to the local database, and then synced to the server.
Our app does this by sending messages back through WebKit’s JavaScript / Objective-C bridge. When the user enters content, we construct a plain JavaScript object which contains their data, then send that through the bridge. When it reaches the app’s main process it’s automatically converted to an NSDictionary, and from there we’re able to process and save it.
However we found that the WebKit process which maintains the connection between the editor and the app would occasionally terminate. This clearly affected our ability to save the user’s changes, because in this state the editor would no longer respond to messages.
Prior to iOS 9 we were able to detect this thanks to a trick we picked up on the WebKit bug tracker, namely using key-value notifications (KVO) to detect when the URL of the web view is set to nil. This was a hack, and thankfully in iOS 9 Apple introduced a delegate method to properly detect these events: webViewWebContentProcessDidTerminate. Using either of these solutions we’re able to recover the web view by reloading it, but at the cost of any data the user has entered into the web view since it was last saved.
This is a risk whenever WebKit is used, but it becomes more complicated when relying on a web server because as we’ll see with the next issue, the server might not be available – which makes the recovery process more protracted.
Sockets and the background
One of the biggest limitations of the web server approach is caused by Apple’s restrictions on what apps can do while backgrounded. The socket that the web server opens to communicate over HTTP will be closed when the app enters the background, so each time the user leaves and returns to the app, the server has to restart.
This is problematic because it means there’s a delay before the editor becomes reusable again. We have to wait until the web server is started because otherwise things like images won’t resolve if the port the server’s running on has changed. This affects the user experience because the app feels less responsive. It also means that we weren’t able to interact with the editor while the app was closed, to pre-load notes, for example.
Failure to load URLs
Finally we saw that sometimes, for reasons we weren’t able to identify but suspect were due to stale and/or broken sockets, the server would fail to resolve the editor’s URL. In these situations the only thing we could do was to retry until the load succeeded, which again introduced a delay to the user’s editing experience.
In some rare cases the server would consistently fail to load the editor’s content, leaving us with no recourse other than to ask the user to force quit the app and restart it. This was the last straw; we knew we needed to adopt a more reliable approach.
The solution
Getting rid of unnecessary complexity is always a good thing. While the web server approach could be considered complex, was it unnecessary? In the early days of the app’s life, we’d say it wasn’t. The alternatives we ruled out were:
- To point the web view to a string of HTML – complicated because of the resources we need to load (JavaScript files, CSS, etc.). We’d have needed to compile that ourselves, and it’d have made debugging more difficult.
- To point the web view to a local file on disk. This wasn’t satisfactory because Apple restricted what could be loaded from these files, for security reasons. There was also no way to handle the loading of images.
- Using UIWebView instead of WKWebView which had features to address the above problem with using local files, but was deprecated by Apple and therefore not something we could rely on.
- Writing our own custom editor that didn’t rely on a web page to function – perhaps extending UITextView, as WordPress have done with their Aztec Editor. That would’ve been a significant amount of work, and there’s no guarantees that we’d have had equal functionality given that UITextView doesn’t have the same level of JavaScript integration as WKWebView.
As we re-evaluated these options we realised that while many still held true, one in particular was more feasible: loading the editor from a file on disk. This was because two things had changed since we’d last considered it:
- In iOS 8 Apple introduced a parameter to the file loading API which allows us to designate a directory from which it is ‘safe’ to load additional assets and resources. This meant that the editor’s additional JavaScript, CSS and image files would be accessible to the main editor HTML file.
- Since iOS 11 it’s been possible to register ‘scheme handlers’ with a WKWebView. These services are invoked when the web page encounters a resource URL with an unrecognised scheme. This is a neat way to allow your app to customise the loading of certain components of the page.
The first of these points meant that in theory, there was no longer any barrier to us loading the editor from a file on disk. Our resources would remain the same – we’d just load them a different way. Without needing to concern ourselves with the overhead of managing the web server, we could rely on the editor to load more quickly and reliably.
There was one sticking point though, and that was what we did with images that had been embedded within our users’ notes. We solved this with the web server approach by placing the images within a directory under the document root of the server – by aping the paths that are used by our web app, we could trust that images would resolve just as they do there.
But if we were to load the editor from a file in the application bundle, this would no longer work unless we copied all the files somewhere else first. Rather than do that, we took advantage of the new Scheme Handler to elegantly handle embedded image loading. The process involved is:
- Find all the embedded images within the note, and rewrite their URLs so that instead of a http:// or https:// protocol they have a custom protocol, such as “bipsync-image://”
- Register a Scheme Handler for the “bipsync-image://” protocol
- Whenever the editor encounters an image with the custom protocol, it passes the request off to the Scheme Handler. We parse the URL, extract the image ID, and load the binary data for the image from disk. We place that data into a response object, then send that response back to the web view – effectively behaving just like a web server would.
Wrapping up
With all this in place, we found that our note editor was not only quicker to load, but most importantly was much more reliable. As the app transitions to and from the background there’s no longer a delay to the editing experience while we rekindle things. And to top it all off, we’ve deleted thousands of lines of redundant code from the app.
We’re going serverless.