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