So here’s something fun you can do with the whole family. (Okay, I have a weird family. YMMV.)
So I want to allow my UIWebView to handle URLs of the form “myscheme://myhost/mypath”, and intercept these requests to (for example) pull the data for each of these requests from a zip archive.
At first I thought “hey, just use the UIWebViewDelegate”, but that turns out not to work very well.
NSURLProtocol to the rescue!
There are plenty of tutorials out there, but they all seem to cover the idea of using NSURLProtocol as a sort of caching system. There are so many other things you can do with it, though!
Like, in my case, create a brand new way to pull data that does not rely on a network connection.
NSURLProtocol is an abstract class which allows you to insert a custom mechanism for loading URLs. What you do, you see, is build a new NSURLProtocol that handles some new protocol (like, oh, say, handling request to myscheme), and insert it into the networking stack so when, anywhere in your app, you see a request for “myscheme://blahblahblah”, it is handled by your custom code.
So here’s how you use the class.
Step 1
Create a new class which inherits from NSURLProtocol. (We’ll call this “MySchemeProtocol.”)
Define the glass method canInitWithRequest: which indicates that your class needs to be used to process your custom request. (Note: your protocols will be examined first when looking for a protocol to handle a request, so in theory you could intercept file:/// and http:// requests. Probably best not to do this.)
+ (BOOL)canInitWithRequest:(NSURLRequest *)request { if (![request.URL.scheme isEqualToString:@"myscheme"]) return NO; return YES; }
Note that if this returns true, a new instance of an NSURLProtocol class will be created for each request. You can thus use the NSURLProtocol class to track any local state associated with the specific request.
Step 2
The documentation also says you must implement canInitWithTask: and canonicalRequestForRequest:. You can read the documentation to understand what these methods do, but in my case (and I suspect, in yours), you really don’t need to do much. The former can examine the request behind the task, the latter can just pass the URL back.
Honestly I don’t know the consequences of defining canInitWithTask: the way I did, so beware.
+ (BOOL)canInitWithTask:(NSURLSessionTask *)task { return [self canInitWithRequest:task.originalRequest]; } + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; }
Step 3
Once you’ve added the boilerplate above, you must implement startLoading and stopLoading.
Now in my case, I assume that the contents from each request are loaded immediately after startLoading, though you can also kick off a thread to obtain the data. (If you do kick off a thread or a task or some other asynchronous mechanism for obtaining data, you must halt that process when stopLoading is called.)
So in my case, with data loaded immediately on startLoading, our required stopLoading method is easy:
- (void)stopLoading { // Does nothing, since I satisfy the request synchronously in startLoading. // Otherwise, stop the background task or thread here. }
Step 4
You start loading (or in my case, synchronously load) when startLoading is called.
Now here’s the thing I had to discover on my own. If you are loading data yourself (rather than just fiddling with the caching of data), you wind up having to interact with the NSURLProtocolClient object that is stored in your self.client field. This is the client that made the request, and it is where the data needs to go once you obtain it.
While loading data, if the cached data passed to your protocol is valid (for some definition of “valid” you get to define yourself), you can call the URLProtocol:cachedResponseIsValid: method, and return.
- (void)startLoading { // In my case I assume the cached response is always valid. So if // we have a cached response, simply pass it up. You can add logic, // such as "if this is more than 5 minutes old, it's not valid." if (self.cachedResponse) { [self.client URLProtocol:self cachedResponseIsValid:self.cachedResponse]; } else {
Now if we have to load our data, we then respond to the NSURLProtocolClient, first by calling URLProtocol:didReceiveResponse:cacheStoragePolicy: to indicate we are receiving something. Then we call URLProtocol:didLoadData: one or more times with the data we receive. (If this is being loaded asynchronously, we can call didLoadData multiple times as we receive our data.) And finally, once all the data is loaded, we call URLProtocolDidFinishLoading.
So in our case, as we’re loading all our data from an archive (which I don’t describe here how it works, other than it’s synchronous) is:
// Load data from our internal file and pass the results NSData *data = [[MyArchive shared] loadDataFromURL:self.request.URL]; NSString *mimeType = [[MyArchive shared] mimeTypeForURL:self.request.URL]; NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL MIMEType:mimeType expectedContentLength:data.length textEncodingName:"utf-8"]; [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed]; [self.client URLProtocol:self didLoadData:data]; [self.client URLProtocolDidFinishLoading:self]; } }
Step 5
Now that you’re done, you simply need to register this when your application starts up:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Other stuff goes here... [NSURLProtocol registerClass:CSBookURLProtocol.class]; return YES; }
And now you can invoke your protocol when loading a specialized URL, such as:
NSString *myURL = @"myscheme://myhost/mypath"; NSURL *url = [NSURL URLWithString:myURL]; NSURLRequest *req = [NSURLRequest requestWithURL:url]; [self.myWebView loadRequest:req];