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 3 of 5 of this article. Click here to view the first page.

Custom URL Loading

“I love it when pages take forever to load” said no user, ever. So now you need to make sure your app can actually handle the requests. As soon as you return YES in your +canInitWithRequest: method, it’s entirely your class’s responsibility to handle everything about that request. This means you need to get the requested data and provide it back to the URL Loading System.

How do you get the data?

If you’re implementing a new application networking protocol from scratch (e.g. adding a foo:// protocol), then here is where you embrace the harsh joys of application network protocol implementation. But since your goal is just to insert a custom caching layer, you can just get the data by using a NSURLConnection.

Effectively you’re just going to intercept the request and then pass it back off to the standard URL Loading System through using NSURLConneciton.

Data is returned from your custom NSURLProtocol subclass through a NSURLProtocolClient. Every NSURLProtocol object has access to it’s “client”, an instance of NSURLProtocolClient. (Well, actually NSURLProtocolClient is a protocol. So it’s an instance of something that conforms to NSURLProtocolClient).

Through the client, you communicate to the URL Loading System to pass back state changes, responses and data.

Open MyURLProtocol.m. Add the following class continuation category at the top of the file:

@interface MyURLProtocol () <NSURLConnectionDelegate>
@property (nonatomic, strong) NSURLConnection *connection;
@end

Next, find +canInitWithRequest:. Change the return to YES, like this:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    static NSUInteger requestCount = 0;
    NSLog(@'Request #%u: URL = %@', requestCount++, request.URL.absoluteString);
    return YES;
}

Now add four more methods:

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
    return [super requestIsCacheEquivalent:a toRequest:b];
}

- (void)startLoading {
    self.connection = [NSURLConnection connectionWithRequest:self.request delegate:self];
}

- (void)stopLoading {
    [self.connection cancel];
    self.connection = nil;
}

+canonicalRequestForRequest: is an abstract method from NSURLProtocol. Your class must implement it. It’s up to your application to define what a “canonical request” means, but at a minimum it should return the same canonical request for the same input request. So if two semantically equal (i.e. not necessarily ==) are input to this method, the output requests should also be semantically equal.

To meet this bare minimum, just return the request itself. Usually, this is a reliable go-to solution, because you usually don’t want to change the request. After all, you trust the developer, right?! An example of something you might do here is to change the request by adding a header and return the new request.

+requestIsCacheEquivalent:toRequest:. is where you could take the time to define when two distinct requests of a custom URL scheme (i.e foo:// are equal, in terms of cache-ability. If two requests are equal, then they should use the same cached data. This concerns URL Loading System’s own, built-in caching system, which you’re ignoring for this tutorial. So for this exercise, just rely on the default superclass implementation.

-startLoading and -stopLoading are what the loading system uses to tell your NSURLProtocol to start and stop handling a request. The start method is called when a protocol should start loading data. The stop method exists so that URL loading can be cancelled. This is handled in the above example by cancelling the current connection and getting rid of it.

Woo-hoo! You’ve implemented the interface required of a valid NSURLProtocol instance. Checkout out the official documentation describing what methods an valid NSURLProtocol subclass can implement, if you want to read more.

But your coding isn’t done yet! You still need to do the actual work of processing the request, which you do by handling the delegate callbacks from the NSURLConnection you created.

Open MyURLProtocol.m. Add the following methods:

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [self.client URLProtocol:self didLoadData:data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    [self.client URLProtocolDidFinishLoading:self];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [self.client URLProtocol:self didFailWithError:error];
}

These are all NSURLConnection delegate methods. They are called when the NSURLConnection you’re using to load the data has a response, when it has data, when it finishes loading and when it fails. In each of these cases, you’re going to need to hand this information off to the client.

So to recap, your MyURLProtocol handler creates its own NSURLConnection and asks that connection to process the request. In the NSURLConnection delegate callbacks methods above, the protocol handler is relaying messages from the connection back to the URL Loading System. These messages talk about loading progress, completion, and errors.

Look and you’ll see the close family resemblance in message signatures for the NSURLConnectionDelegate and the NSURLProtocolClient — they are both APIs for asynchronous data loading. Also notice how MyURLProtocol uses its client property to send messages back to the URL Loading system.

Build and run. When the app opens, enter the same URL and hit Go.

Uh-oh! Your browser isn’t loading anything anymore! If you look at the Debug Navigator while it’s running, you’ll see memory usage is out of control. The console log should show a racing scroll of innumerable requests for the same URL. What could be wrong?

In the console you should see lines being logged forever and ever like this:

2014-01-19 07:15:59.321 NSURLProtocolExample[992:70b] Request #0: URL = http://www.raywenderlich.com/
2014-01-19 07:15:59.322 NSURLProtocolExample[992:70b] Request #1: URL = http://www.raywenderlich.com/
2014-01-19 07:15:59.329 NSURLProtocolExample[992:70b] Request #2: URL = http://www.raywenderlich.com/
2014-01-19 07:15:59.329 NSURLProtocolExample[992:70b] Request #3: URL = http://www.raywenderlich.com/
2014-01-19 07:15:59.330 NSURLProtocolExample[992:70b] Request #4: URL = http://www.raywenderlich.com/
2014-01-19 07:15:59.333 NSURLProtocolExample[992:570b] Request #5: URL = http://www.raywenderlich.com/
2014-01-19 07:15:59.333 NSURLProtocolExample[992:570b] Request #6: URL = http://www.raywenderlich.com/
2014-01-19 07:15:59.333 NSURLProtocolExample[992:570b] Request #7: URL = http://www.raywenderlich.com/
2014-01-19 07:15:59.333 NSURLProtocolExample[992:570b] Request #8: URL = http://www.raywenderlich.com/
2014-01-19 07:15:59.334 NSURLProtocolExample[992:570b] Request #9: URL = http://www.raywenderlich.com/
2014-01-19 07:15:59.334 NSURLProtocolExample[992:570b] Request #10: URL = http://www.raywenderlich.com/
...
2014-01-19 07:15:60.678 NSURLProtocolExample[992:570b] Request #1000: URL = http://www.raywenderlich.com/
2014-01-19 07:15:60.678 NSURLProtocolExample[992:570b] Request #1001: URL = http://www.raywenderlich.com/

Squashing the Infinite Loop with Tags

Think again about the URL Loading System and protocol registration, and you might have a notion about why this is happening. When the UIWebView wants to load the URL, the URL Loading System asks MyURLProtocol if it can handle that specific request. Your class says YES, it can handle it.

So the URL Loading System will create an instance of your protocol and call startLoading. Your implementation then creates and fires its NSURLConnection. But this also calls the URL Loading System. Guess what? Since you’re always returning YES in the +canInitWithRequest: method, it creates another MyURLProtocol instance.

This new instance will lead to a creation of one more, and then one more and then an ifinite number of instances. That’s why you app doesn’t load anything! It just keeps allocating more memory, and shows only one URL in the console. The poor browser is stuck in an infinite loop! Your users could be frustrated to the point of inflicting damage on their devices.

Review what you’ve done and then move on to how you can fix it. Obviously you can’t just always return YES in the +canInitWithRequest: method. You need to have some sort of control to tell the URL Loading System to handle that request only once. The solution is in the NSURLProtocol interface. Look for the class method called +setProperty:forKey:inRequest: that allows you to add custom properties to a given URL request. This way, you can ‘tag’ it by attaching a property to it, and the browser will know if it’s already seen it before.

So here’s how you break the browser out of infinite instance insanity. Open MyURLProtocol.m. Then change the -startLoading and the +canInitWithRequest: methods as follows:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    static NSUInteger requestCount = 0;
    NSLog(@"Request #%u: URL = %@", requestCount++, request);
    
    if ([NSURLProtocol propertyForKey:@"MyURLProtocolHandledKey" inRequest:request]) {
        return NO;
    }
    
    return YES;
}

- (void)startLoading {
    NSMutableURLRequest *newRequest = [self.request mutableCopy];
    [NSURLProtocol setProperty:@YES forKey:@"MyURLProtocolHandledKey" inRequest:newRequest];
    
    self.connection = [NSURLConnection connectionWithRequest:newRequest delegate:self];
}

Now the -startLoading method sets a NSNumber instance () for a given key (@"MyURLProtocolHandledKey") and for a given request. It means the next time it calls +canInitWithRequest: for a given NSURLRequest instance, the protocol can ask if this same property is set.

If it is set, and it’s set to YES, then it means that you don’t need to handle that request anymore. The URL Loading System will load the data from the web. Since your MyURLProtocol instance is the delegate for that request, it will receive the callbacks from NSURLConnectionDelegate.

Build and run. When you try it now, the app will successfully display web pages in your web view. Sweet victory! You might be wondering why you did all of this just to get the app to behave just like it was when you started. Well, because you need to prepare for the fun part!

The console should now look something like this:

2014-01-19 07:22:42.260 NSURLProtocolExample[1019:70b] Request #0: URL = <NSMutableURLRequest: 0x9c17770> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:42.261 NSURLProtocolExample[1019:70b] Request #1: URL = <NSMutableURLRequest: 0x8b49000> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:42.270 NSURLProtocolExample[1019:70b] Request #2: URL = <NSURLRequest: 0xea1cd20> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:42.271 NSURLProtocolExample[1019:70b] Request #3: URL = <NSURLRequest: 0xea1c960> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:42.271 NSURLProtocolExample[1019:70b] Request #4: URL = <NSURLRequest: 0xea221c0> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:42.274 NSURLProtocolExample[1019:4113] Request #5: URL = <NSURLRequest: 0xea2c610> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:42.274 NSURLProtocolExample[1019:4113] Request #6: URL = <NSURLRequest: 0xea2c610> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:42.274 NSURLProtocolExample[1019:4113] Request #7: URL = <NSURLRequest: 0xea2c610> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:42.274 NSURLProtocolExample[1019:4113] Request #8: URL = <NSURLRequest: 0xea294c0> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:42.275 NSURLProtocolExample[1019:4113] Request #9: URL = <NSURLRequest: 0xea2c610> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:42.275 NSURLProtocolExample[1019:4113] Request #10: URL = <NSURLRequest: 0xea294c0> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:42.276 NSURLProtocolExample[1019:6507] Request #11: URL = <NSURLRequest: 0x8c46af0> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:42.276 NSURLProtocolExample[1019:1303] Request #12: URL = <NSURLRequest: 0x8a0b090> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:42.277 NSURLProtocolExample[1019:4113] Request #13: URL = <NSURLRequest: 0x8a0c4a0> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:42.277 NSURLProtocolExample[1019:4113] Request #14: URL = <NSURLRequest: 0x8a0c4a0> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:43.470 NSURLProtocolExample[1019:330b] Request #15: URL = <NSURLRequest: 0x8b4ea60> { URL: http://www.raywenderlich.com/ }
2014-01-19 07:22:43.471 NSURLProtocolExample[1019:330b] Request #16: URL = <NSURLRequest: 0x8d38320> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 }
2014-01-19 07:22:43.471 NSURLProtocolExample[1019:330b] Request #17: URL = <NSURLRequest: 0x8d386c0> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 }
2014-01-19 07:22:43.471 NSURLProtocolExample[1019:330b] Request #18: URL = <NSURLRequest: 0x8d38ad0> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 }
2014-01-19 07:22:43.471 NSURLProtocolExample[1019:4113] Request #19: URL = <NSURLRequest: 0x8b50250> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 }
2014-01-19 07:22:43.472 NSURLProtocolExample[1019:4113] Request #20: URL = <NSURLRequest: 0x8b50250> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 }
2014-01-19 07:22:43.472 NSURLProtocolExample[1019:330b] Request #21: URL = <NSURLRequest: 0xea9c420> { URL: http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.8 }
2014-01-19 07:22:43.472 NSURLProtocolExample[1019:4113] Request #22: URL = <NSURLRequest: 0x8b50250> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 }
2014-01-19 07:22:43.472 NSURLProtocolExample[1019:330b] Request #23: URL = <NSURLRequest: 0xea9c3f0> { URL: http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.8 }
2014-01-19 07:22:43.472 NSURLProtocolExample[1019:4113] Request #24: URL = <NSURLRequest: 0x8b4fcb0> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 }
2014-01-19 07:22:43.472 NSURLProtocolExample[1019:330b] Request #25: URL = <NSURLRequest: 0xea9c4d0> { URL: http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.8 }
2014-01-19 07:22:43.472 NSURLProtocolExample[1019:4113] Request #26: URL = <NSURLRequest: 0x8b50250> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 }
2014-01-19 07:22:43.472 NSURLProtocolExample[1019:4113] Request #27: URL = <NSURLRequest: 0x8b4fcb0> { URL: http://cdn2.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1389898954 }
...

Now you have all the control of the URL data of your app and you can do whatever you want with it. It’s time to start caching your app’s URL data.

Rocir Santiago

Contributors

Rocir Santiago

Author

Over 300 content creators. Join our team.