The first logical choice was to use file:// URLs, and this almost works. The first annoyance comes when you want upload data, where you now need to implement CORS on the server because the request is coming from a source that's not in the same domain (cross-site scripting problem).
The second problem is that HTML5 local storage doesn't work quite right with file URLs either. That problem is rooted in the fact that there isn't a domain, and normally local storage is domain-based. Actually, I think this might work in an iOS app and Safari, but when I tried it I had another bug that was preventing it from working properly.
After doing some poking around, I found two interesting solutions:
Rob Napier's RNCachingURL Protocol
RNCachingURLProtocol is sort of the opposite of what I wanted, it makes requests and caches the result locally, which, while useful in some cases, isn't what I wanted. I wanted to always use the version embedded in the main bundle of my app. But I did look at that code quite carefully, and it was integral to my understanding how to do my solution.
AFCache seems like it might do what I wanted, but it's big, complicated, and hasn't been updated in a while. I wanted something small, fast, and ARC-aware.
So here's my solution:
I created two Objective-C classes:
LocalFileURLProtocol: This implements the NSURLProtocol to intercept the requests for files that are in our bundle. Note that we don't use file:// URLs, we use the actual http:// URLs we'd use for online use, but intercept them and return the html/css/js from our app's bundle instead of loading across the network.
LocalURLDecoder: This embeds our local knowledge of our server and URL format, so we know what things to try to intercept. You'll obviously need to customize this for your own app, but it's pretty straightforward.
The only thing you need to do is register the LocalFileURLProtocol, probably in AppDelegate.m in didFinishLaunchingWithOptions:
That's it! Here are the files: