Using NSURLProtocol with Swift

In this NSURLProtocol tutorial you will learn how to work with the URL loading system and URL schemes to add custom behavior to your apps. By Zouhair Mahieddine.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 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 just once, and then cache it. If the same request is fired again in the future, your protocol will serve the cached response without reloading it from the web.

Note: The starter project already includes a basic Core Data model and stack. You don’t need to know the details of Core Data and can just think of it as an opaque data store; if you’re interested, check out Apple’s Core Data Programming Guide.

It’s time to save the responses your app receives from the web, and retrieve them whenever it has matching cached data. Open MyURLProtocol.swift and add the following import to the top of the file:

import CoreData

Next, add two properties inside the class definition:

var mutableData: NSMutableData!
var response: NSURLResponse!

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).

Then add the following method to the class:

func saveCachedResponse () {
    println("Saving cached response")
    
    // 1
    let delegate = UIApplication.sharedApplication().delegate as AppDelegate
    let context = delegate.managedObjectContext!
    
    // 2
    let cachedResponse = NSEntityDescription.insertNewObjectForEntityForName("CachedURLResponse", inManagedObjectContext: context) as NSManagedObject
    
    cachedResponse.setValue(self.mutableData, forKey: "data")
    cachedResponse.setValue(self.request.URL.absoluteString, forKey: "url")
    cachedResponse.setValue(NSDate(), forKey: "timestamp")
    cachedResponse.setValue(self.response.MIMEType, forKey: "mimeType")
    cachedResponse.setValue(self.response.textEncodingName, forKey: "encoding")
    
    // 3
    var error: NSError?
    let success = context.save(&error)
    if !success {
        println("Could not cache the response")
    }
}

Here’s what this method does:

  1. Obtain the Core Data NSManagedObjectContext from the AppDelegate instance. The managed object context is your interface to Core Data.
  2. Create an instance of NSManagedObject to match the data model you saw in the .xcdatamodeld file. Set its properties based on the references to the NSURLResponse and NSMutableData that you kept.
  3. Save the Core Data managed object context.

Now that you have a way to store the data, you need to call this method from somewhere. Still in MyURLProtocol.swift, change the NSURLConnection delegate methods to the following implementations:

func connection(connection: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
    self.client!.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
    
    self.response = response
    self.mutableData = NSMutableData()
}
  
func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {
    self.client!.URLProtocol(self, didLoadData: data)
    self.mutableData.appendData(data)
}
  
func connectionDidFinishLoading(connection: NSURLConnection!) {
    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.

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.swift. Then add the following method:

func cachedResponseForCurrentRequest() -> NSManagedObject? {
    // 1
    let delegate = UIApplication.sharedApplication().delegate as AppDelegate
    let context = delegate.managedObjectContext!
    
    // 2
    let fetchRequest = NSFetchRequest()
    let entity = NSEntityDescription.entityForName("CachedURLResponse", inManagedObjectContext: context)
    fetchRequest.entity = entity
    
    // 3
    let predicate = NSPredicate(format:"url == %@", self.request.URL.absoluteString!)
    fetchRequest.predicate = predicate
    
    // 4
    var error: NSError?
    let possibleResult = context.executeFetchRequest(fetchRequest, error: &error) as Array<NSManagedObject>?
    
    // 5
    if let result = possibleResult {
        if !result.isEmpty {
            return result[0]
        }
    }
    
    return nil
}

Here’s what this does:

  1. Grab the Core Data managed object context, just like in saveCachedResponse().
  2. Create an NSFetchRequest saying that you want to find entities called CachedURLResponse. This is the entity in the managed object model that you want to retrieve.
  3. The predicate for the fetch request needs to obtain the CachedURLResponse object that relates to the URL that you’re trying to load. This code sets that up.
  4. Execute the fetch request is.
  5. If there are any results, return the first result.

Now it’s time to look back at the startLoading() implementation. Rather than just load everything from the web, it needs to check for a cached response for the URL first. Find the current implementation and replace it with the following:

override func startLoading() {
    // 1
    let possibleCachedResponse = self.cachedResponseForCurrentRequest()
    if let cachedResponse = possibleCachedResponse {
        println("Serving response from cache")
      
        // 2
        let data = cachedResponse.valueForKey("data") as NSData!
        let mimeType = cachedResponse.valueForKey("mimeType") as String!
        let encoding = cachedResponse.valueForKey("encoding") as String!
      
        // 3
        let response = NSURLResponse(URL: self.request.URL, MIMEType: mimeType, expectedContentLength: data.length, textEncodingName: encoding)
      
        // 4
        self.client!.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
        self.client!.URLProtocol(self, didLoadData: data)
        self.client!.URLProtocolDidFinishLoading(self)
    } else {
        // 5
        println("Serving response from NSURLConnection")
      
        var newRequest = self.request.mutableCopy() as NSMutableURLRequest
        NSURLProtocol.setProperty(true, forKey: "MyURLProtocolHandledKey", inRequest: newRequest)
        self.connection = NSURLConnection(request: newRequest, delegate: self)
    }
}

Here’s what that does:

  1. First, you need to find out if there’s a cached response for the current request.
  2. If there is, then pull all the relevant data out of the cached object.
  3. Create an NSURLResponse object from the saved data.
  4. Tell the client about the response and data. You set the client’s cache storage policy to .NotAllowed since you don’t want the client to do any caching of its own since that’s your job. Then you can call URLProtocolDidFinishLoading right away to signal that it has finished loading. No network calls – that’s it!
  5. If there was no cached response, then load the data as usual.

Build and run your project again. Browse a couple of web sites and then quit the app. Switch your device to Airplane mode (or, if using the iOS simulator, turn your computer’s Wi-Fi off / unplug the Ethernet cable) 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:

Request #22: URL = http://vjs.zencdn.net/4.5/video-js.css?ver=3.9.1
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! :]

When To Use NSURLProtocol?

How can you use NSURLProtocol to make your app cooler, faster, stronger and jaw-droppingly awesome? Here are a few examples:

Provide Custom Responses For Your Network Requests:

It doesn’t matter if you’re making a request using a UIWebView, NSURLConnection or even using a third-party library (like AFNetworking, MKNetworkKit, your own, etc, as these are all built on top of NSURLConnection). You can provide a custom response, both for metadata and for data. You might use this if you want to stub out the response of a request for testing purposes, for example.

Skip Network Activity and Provide Local Data:

Sometimes you may think it’s unnecessary to fire a network request to provide the app whatever data it needs. NSURLProtocol can set your app up to find data on local storage or in a local database.

Redirect Your Network Requests:

Have you ever wished you could redirect requests to a proxy server — without trusting the user to follow specific iOS setup directions? Well, you can! NSURLProtocol gives you what you want — control over requests. You can set up your app to intercept and redirect them to another server or proxy, or wherever you want to. Talk about control!!

Change the User-agent of Your Requests:

Before firing any network request, you can decide to change its metadata or data. For instance, you may want to change the user-agent. This could be useful if your server changes content based on the user-agent. An example of this would be differences between the content returned for mobile versus desktop, or the client’s language.

Use Your Own Networking Protocol:

You may have your own networking protocol (for instance, something built on top of UDP). You can implement it and, in your application, you still can can keep using any networking library you prefer.

Needless to say, the possibilities are many. It would be impractical (but not impossible) to list all the possibilities you have with NSURLProtocol in this tutorial. You can do anything you need with a given NSURLRequest before it’s fired by changing the designated NSURLResponse. Better yet, just create your own NSURLResponse. You’re the developer, after all.

While NSURLProtocol is powerful, remember that it’s not a networking library. It’s a tool you can use in addition to the library you already use. In short, you can take advantage of NSURLProtocol‘s benefits while you use your own library.

Zouhair Mahieddine

Contributors

Zouhair Mahieddine

Author

Over 300 content creators. Join our team.