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.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

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:

Screen Shot 2013-12-15 at 11.22.48 PM

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:

  1. Obtain the Core Data NSManagedObjectContext from the AppDelegate instance.
  2. Create an instance of CachedURLResponse and set its properties based on the references to the NSURLResponse and NSMutableData that you kept.
  3. 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:

  1. Grab the Core Data managed object context, just like in saveCachedResponse.
  2. 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.
  3. 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.
  4. Finally, the fetch request is executed.
  5. 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:

  1. First, we need to find out if there’s a cached response for the current request.
  2. If there is then we pull all the relevant data out of the cached object.
  3. An NSURLResponse object is created with the data we have saved.
  4. 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!
  5. 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! :]

Rocir Santiago

Contributors

Rocir Santiago

Author

Over 300 content creators. Join our team.