NSURLProtocol Tutorial
NSURLProtocol is the lesser known heart of the URL handling system in iOS. In this NSURLProtocol tutorial you will learn how to tame it. By Rocir Santiago.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Implementing the Local Cache
Remember the basic requirement for this app: for a given request, it should load the data from the web once and cache it. If the same request is fired again in the future, the cached response will be provided to the app without reloading it from the web.
Now, you can take advantage of Core Data (already included in this app). Open NSURLProtocolExample.xcdatamodeld. Select the Event entity, then click on it again so that it lets you rename it. Call it CachedURLResponse.
Next, click on the + button under Attributes to add a new attribute and name it data with Type set to Binary Data. Do the same thing again to create the properties encoding (String), mimeType (String) and url(String). Rename timeStamp to timestamp. At the end, your entity should look like this:
Now you’re going to create your NSManagedObject subclass for this entity. Select File\New\File…. On the left side of the dialog, select Core Data\NSManagedObject. Click on Next, leave the checkbox for NSURLProtocolExample selected and hit Next. In the following screen, select the checkbox next to CachedURLResponse and click Next. Finally, click Create.
Now you have a model to encapsulate your web data responses and their metadata!
It’s time to save the responses your app receives from the web, and retrieve them whenever it has matching cached data. Open MyURLProtocol.h and add two properties like so:
@property (nonatomic, strong) NSMutableData *mutableData;
@property (nonatomic, strong) NSURLResponse *response;
The response
property will keep the reference to the metadata you’ll need when saving the response from a server. The mutableData
property will be used to hold the data that the connection receives in the -connection:didReceiveData:
delegate method. Whenever the connection finishes, you can cache the response (data and metadata).
Let’s add that now.
Open MyURLProtocol.m. Change the NSURLConnection delegate methods to the following implementations:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
self.response = response;
self.mutableData = [[NSMutableData alloc] init];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.client URLProtocol:self didLoadData:data];
[self.mutableData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.client URLProtocolDidFinishLoading:self];
[self saveCachedResponse];
}
Instead of directly handing off to the client, the response and data are stored by your custom protocol class now.
You’ll notice a call to an unimplemented method, saveCachedResponse. Let’s go ahead and implement that.
Still in MyURLProtocol.m, add imports for AppDelegate.h and CachedURLResponse.h. Then add the following method:
- (void)saveCachedResponse {
NSLog(@"saving cached response");
// 1.
AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = delegate.managedObjectContext;
// 2.
CachedURLResponse *cachedResponse = [NSEntityDescription insertNewObjectForEntityForName:@"CachedURLResponse"
inManagedObjectContext:context];
cachedResponse.data = self.mutableData;
cachedResponse.url = self.request.URL.absoluteString;
cachedResponse.timestamp = [NSDate date];
cachedResponse.mimeType = self.response.MIMEType;
cachedResponse.encoding = self.response.textEncodingName;
// 3.
NSError *error;
BOOL const success = [context save:&error];
if (!success) {
NSLog(@"Could not cache the response.");
}
}
Here is what that does:
- Obtain the Core Data
NSManagedObjectContext
from theAppDelegate
instance. - Create an instance of
CachedURLResponse
and set its properties based on the references to theNSURLResponse
andNSMutableData
that you kept. - Save the Core Data managed object context.
Build and run. Nothing changes in the app’s behavior, but remember that now successfully retrieved responses from the web server save to your app’s local database.
Retrieving the Cached Response
Finally, now it’s time to retrieve cached responses and send them to the NSURLProtocol
‘s client. Open MyURLProtocol.m. Then add the following method:
- (CachedURLResponse *)cachedResponseForCurrentRequest {
// 1.
AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context = delegate.managedObjectContext;
// 2.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"CachedURLResponse"
inManagedObjectContext:context];
[fetchRequest setEntity:entity];
// 3.
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"url == %@", self.request.URL.absoluteString];
[fetchRequest setPredicate:predicate];
// 4.
NSError *error;
NSArray *result = [context executeFetchRequest:fetchRequest error:&error];
// 5.
if (result && result.count > 0) {
return result[0];
}
return nil;
}
Here’s what it does:
- Grab the Core Data managed object context, just like in
saveCachedResponse
. - Create an
NSFetchRequest
saying that we want to find entities called CachedURLResponse. This is the entity in the managed object model that we want to retrieve. - The predicate for the fetch request needs to obtain the
CachedURLRepsonse
object that relates to the URL that we’re trying to load. This code sets that up. - Finally, the fetch request is executed.
- If there are any results, then the first result is returned.
Now it’s time to look back at the -startLoading
implementation. It needs to check for a cached response for the URL before actually loading it from the web. Find the current implementation and replace it withe the following:
- (void)startLoading {
// 1.
CachedURLResponse *cachedResponse = [self cachedResponseForCurrentRequest];
if (cachedResponse) {
NSLog(@"serving response from cache");
// 2.
NSData *data = cachedResponse.data;
NSString *mimeType = cachedResponse.mimeType;
NSString *encoding = cachedResponse.encoding;
// 3.
NSURLResponse *response = [[NSURLResponse alloc] initWithURL:self.request.URL
MIMEType:mimeType
expectedContentLength:data.length
textEncodingName:encoding];
// 4.
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[self.client URLProtocol:self didLoadData:data];
[self.client URLProtocolDidFinishLoading:self];
} else {
// 5.
NSLog(@"serving response from NSURLConnection");
NSMutableURLRequest *newRequest = [self.request mutableCopy];
[NSURLProtocol setProperty:@YES forKey:@"MyURLProtocolHandledKey" inRequest:newRequest];
self.connection = [NSURLConnection connectionWithRequest:newRequest delegate:self];
}
}
Here’s what that does:
- First, we need to find out if there’s a cached response for the current request.
- If there is then we pull all the relevant data out of the cached object.
- An
NSURLResponse
object is created with the data we have saved. - Finally, for the cached case, the client is told of the response and data. Then it immediately is told the loading finished, because it has! No longer do we need to wait for the network to download the data. It’s already been served through the cache! The reason NSURLCacheStorageNotAllowed is passed to the client in the response call, is that we don’t want the client to do any caching of its own. We’re handling the caching, thanks!
- If there was no cached response, then we need to load the data as normal.
Build and run your project again. Browse a couple of web sites and then stop using it (stop the project in Xcode). Now, retrieve cached results. Turn the device’s Wi-Fi off (or, if using the iOS simulator, turn your computer’s Wi-Fi off) and run it again. Try to load any website you just loaded. It should load the pages from the cached data. Woo hoo! Rejoice! You did it!!!
You should see lots of entries in the console that look like this:
2014-01-19 08:35:45.655 NSURLProtocolExample[1461:4013] Request #28: URL = <NSURLRequest: 0x99c33b0> { URL: http://www.raywenderlich.com/wp-content/plugins/wp-polls/polls-css.css?ver=2.63 }
2014-01-19 08:35:45.655 NSURLProtocolExample[1461:6507] serving response from cache
That’s the log saying that the response is coming from your cache!
And that’s that. Now your app successfully caches retrieved data and metadata from web page requests. Your users will enjoy faster page loads and superior performance! :]