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.
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 true
in canInitWithRequest(_:)
, 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 NSURLConnection
.
Effectively you’re just going to intercept the request and then pass it back off to the standard URL Loading System through using NSURLConnection.
Your custom NSURLProtocol
subclass returns data through an object that implements the NSURLProtocolClient
protocol. There’s a bit of confusing naming to keep straight in your head: NSURLProtocol
is a class, and NSURLProtocolClient
is a protocol!
Through the client, you communicate to the URL Loading System to pass back state changes, responses and data.
Open MyURLProtocol.swift and add the following property at the top of the MyURLProtocol
class definition:
var connection: NSURLConnection!
Next, find canInitWithRequest(_:)
. Change the return line to return true
:
return true
Now add four more methods:
override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
return request
}
override class func requestIsCacheEquivalent(aRequest: NSURLRequest,
toRequest bRequest: NSURLRequest) -> Bool {
return super.requestIsCacheEquivalent(aRequest, toRequest:bRequest)
}
override func startLoading() {
self.connection = NSURLConnection(request: self.request, delegate: self)
}
override func stopLoading() {
if self.connection != nil {
self.connection.cancel()
}
self.connection = nil
}
It’s up to your protocol 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. For example, if your custom URL scheme is case insensitive then you might decide that canonical URLs are all lower case.
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.
The loading system uses startLoading()
and stopLoading()
to tell your NSURLProtocol
to start and stop handling a request. Your start implementation sets up the NSURLConnection
instance to load the 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 the official documentation describing what methods a 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.swift and add the following methods:
func connection(connection: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
self.client!.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
}
func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {
self.client!.URLProtocol(self, didLoadData: data)
}
func connectionDidFinishLoading(connection: NSURLConnection!) {
self.client!.URLProtocolDidFinishLoading(self)
}
func connection(connection: NSURLConnection!, didFailWithError error: NSError!) {
self.client!.URLProtocol(self, didFailWithError: error)
}
These are all NSURLConnection
delegate methods. They are called when the NSURLConnection
instance 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 the project. 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:
Request #0: URL = http://raywenderlich.com/
Request #1: URL = http://raywenderlich.com/
Request #2: URL = http://raywenderlich.com/
Request #3: URL = http://raywenderlich.com/
Request #4: URL = http://raywenderlich.com/
Request #5: URL = http://raywenderlich.com/
Request #6: URL = http://raywenderlich.com/
Request #7: URL = http://raywenderlich.com/
Request #8: URL = http://raywenderlich.com/
Request #9: URL = http://raywenderlich.com/
Request #10: URL = http://raywenderlich.com/
...
Request #1000: URL = http://raywenderlich.com/
Request #1001: URL = http://raywenderlich.com/
...
You’ll need to return to Xcode and stop the app from there before diving into the problem.
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 true
, 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 true
in the canInitWithRequest(_:)
method, it creates another MyURLProtocol instance.
This new instance will lead to the creation of one more, and then one more and then an infinite number of instances. That’s why your 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.
Obviously you can’t just always return true
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.swift. Then change the startLoading()
and the canInitWithRequest(_:)
methods as follows:
override class func canInitWithRequest(request: NSURLRequest) -> Bool {
println("Request #\(requestCount++): URL = \(request.URL.absoluteString)")
if NSURLProtocol.propertyForKey("MyURLProtocolHandledKey", inRequest: request) != nil {
return false
}
return true
}
override func startLoading() {
var newRequest = self.request.mutableCopy() as NSMutableURLRequest
NSURLProtocol.setProperty(true, forKey: "MyURLProtocolHandledKey", inRequest: newRequest)
self.connection = NSURLConnection(request: newRequest, delegate: self)
}
Now startLoading()
sets the property associated with the key "MyURLProtocolHandledKey"
to true
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 true
, 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! The console should now look something like this:
Request #0: URL = http://raywenderlich.com/
Request #1: URL = http://raywenderlich.com/
Request #2: URL = http://raywenderlich.com/
Request #3: URL = http://raywenderlich.com/
Request #4: URL = http://raywenderlich.com/
Request #5: URL = http://raywenderlich.com/
Request #6: URL = http://raywenderlich.com/
Request #7: URL = http://raywenderlich.com/
Request #8: URL = http://raywenderlich.com/
Request #9: URL = http://www.raywenderlich.com/
Request #10: URL = http://www.raywenderlich.com/
Request #11: URL = http://www.raywenderlich.com/
Request #12: URL = http://raywenderlich.com/
Request #13: URL = http://cdn3.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1402962842
Request #14: URL = http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.9.1
Request #15: URL = http://cdn4.raywenderlich.com/wp-content/pluginRse/qvidueeosjts -#h1t6m:l URL = ht5t-pv:i/d/ecodn3.raywenderlich.com/-wppl-acyoenrtent/themes/raywenderlich/-sftoyrl-ew.omridnp.css?vreers=s1/4p0l2u9g6i2n8-4s2t
yles.css?ver=3.9.1
Request #17: URL = http://cdn3.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1402962842
Request #18: URL = http://vjs.zencdn.net/4.5/video-js.css?ver=3.9.1
Request #19: URL = http://www.raywenderlich.com/wp-content/plugins/wp-polls/polls-css.css?ver=2.63
Request #20: URL = http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.9.1
Request #21: URL = http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.9.1
Request #22: URL = http://cdn4.raywenderlich.com/wp-content/plugins/powerpress/player.min.js?ver=3.9.1
Request #23: URL = http://cdn4.raywenderlich.com/wp-content/plugins/videojs-html5-video-player-for-wordpress/plugin-styles.css?ver=3.9.1
Request #24: URL = http://cdn4.raywenderlich.com/wp-content/plugins/videojs-html5-video-player-for-wordpress/plugin-styles.css?ver=3.9.1
Request #25: URL = http://cdn3.raywenderlich.com/wp-content/themes/raywenderlich/style.min.css?ver=1402962842
Request #26: URL = http://cdn3.raywenderlich.com/wp-content/plugins/swiftype-search/assets/autocomplete.css?ver=3.9.1
...
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! 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.