Tuesday, August 25, 2015

Custom NSURLProtocol

Hello there, today we will discuss about NSURLProtocol and what it can offer to your project.


NSURLProtocol is a class from Foundation framework which handles all requests made by your app. Even if you don’t create a custom class(by extending NSURLProtocol) the OS uses a default one. In making your app you don’t create instances of NSURLProtocol, or of your custom class that extends NSURLProtocol, the OS does this for you. When you extend NSURLProtocol you have to override some methods, where you put your code, but this we will see below in the article.


Why do you want to create a class that extends NSURLProtocol ?
Well, suppose you want all your app requests to have a certain header in them, or maybe you want to intercept some request before they are sent, or maybe you want to have a more elaborate cache system than the one that is provided to you by the frameworks within the SDK.


In this post we will see how to make our own class that extends NSURLProtocol which will intercept all request and saves the content locally for later use when there is no internet connection.


When extending NSURLProtocol you have to implement a couple of methods in order to make the new class work.
 override class func canInitWithRequest(request: NSURLRequest) -> Bool  
In this method you tell the OS that you handle the execution of the request yourself.


The reason you do this is because you can have multiple protocol classes registered in your app(we will see a bit later how you register your class). The OS takes all registered NSURLProtocol classes and calls canInitWithRequest and the first one that returns true will be used to make an instance of that class and use it for that particular request.


The next method you want to override is
 override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest  

What canonical request really means for your protocol class is up for you do decide, but for our example I just returned the request I got as parameter.

Next we will override
 override func startLoading()  

In this method you will add your code to handle the request. Here is where you want to set that key for the request that you handle, you do this with: 
 NSURLProtocol.setProperty("true", forKey: MyURLProtocol.CustomKey, inRequest: newRequest)  

The reason you set this property is that in method canInitWithRequest you will not return all the time true, because if you do, you end up in an infinite loop. Here in startLoading we tell the OS that we handle this particular request and it should not create another instance of NSURLProtocol for this request.


The last method you need to override is this
 override func stopLoading()  

Now let’s get to work.
Create a new single view application in Xcode: File -> New Project and select single view. For this project I used swift. In the view controller from the main storyboard I added a UIWebView, a UItextField and 2 buttons for back and forward. If you don’t want to make the project step by step you can download the complete version from here
In ViewController.swift file we set the delegate from the web view and from the text field.
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {  
  return true  
}  

func webViewDidStartLoad(webView: UIWebView) {  
  UIApplication.sharedApplication().networkActivityIndicatorVisible = true  
}  

func webViewDidFinishLoad(webView: UIWebView) {  
  UIApplication.sharedApplication().networkActivityIndicatorVisible = false  
}  

func webView(webView: UIWebView, didFailLoadWithError error: NSError) {  
  if error.code != NSURLErrorCancelled {  
    UIApplication.sharedApplication().networkActivityIndicatorVisible = false  
    let alert = UIAlertView(title: "Error", message: error.localizedDescription, delegate: nil, cancelButtonTitle: "OK")  
    alert.show()  
  }  
}  



In the delegate for the text field we are trying to load the url entered by the user.
func textFieldShouldReturn(textField: UITextField) -> Bool {  
  textField.resignFirstResponder()  
  if let url = NSURL(string:textField.text!) {  
    let request = NSURLRequest(URL: url)  
    self.gWebView.loadRequest(request)  
  }  
  return true  
}  

The buttons in the UI have, each, one method for going back and forward, if possible.
@IBAction func onBackTouchUpInside(sender: UIButton) {  
  if self.gWebView.canGoBack {  
    self.gWebView.goBack()  
  }  
}  

@IBAction func onForwardTouchUpInside(sender: UIButton) {  
  if self.gWebView.canGoForward {  
    self.gWebView.goForward()  
  }  
}  

To detect if we have internet connection I used AFNetworking, a great library for working with requests, but for the purpose of this post I used only the reachability portion of it. When we don’t have an internet connection we make the text field background color red.
AFNetworkReachabilityManager.sharedManager().setReachabilityStatusChangeBlock { (status:AFNetworkReachabilityStatus) -> Void in  
  var color:UIColor
  if status == AFNetworkReachabilityStatus.ReachableViaWiFi || status == AFNetworkReachabilityStatus.ReachableViaWWAN {  
    color = UIColor.clearColor()  
  } else {  
    color = UIColor(red: 1, green: 0, blue: 0, alpha: 0.2)  
  }  
  self.txtAddress.backgroundColor = color
}  
AFNetworking is written in Objective-C, but swift can work with code written in Objective-C. Just drag and drop AFNetworkReachabilityManager.h and AFNetworkReachabilityManager.m files into your project and Xcode will prompt you to make a bridging file, select Yes. In Bridging-Header.h file that was created import any Objective-C files you want to use in swift, for example
#import "AFNetworkReachabilityManager.h". Now you can use AFNetworkReachabilityManager in swift as well.


Next we will make a class that extends NSURLProtocol and we will override the methods mentioned above. Now we want to handle the request in startLoading method from our custom URLProtocol class. There in case we have internet connection we will set our key and we will execute the request using NSURLSession and NSURLSessionDataTask. The code looks like this:
override func startLoading() {  
  if AFNetworkReachabilityManager.sharedManager().reachable {  
    let newRequest = self.request.mutableCopy() as! NSMutableURLRequest  
    NSURLProtocol.setProperty("true", forKey: MyURLProtocol.CustomKey, inRequest: newRequest)  
    let defaultConfigObj = NSURLSessionConfiguration.defaultSessionConfiguration()  
    let defaultSession = NSURLSession(configuration: defaultConfigObj, delegate: self, delegateQueue: nil)  
    self.dataTask = defaultSession.dataTaskWithRequest(newRequest)  
    self.dataTask!.resume()  
  }   
}  


In case we don’t have an internet connection we will load the response from local storage if there is one, if no response is available we will create a failed response and pass it along to the client property from NSURLProtocol.
let httpVersion = "1.1"  
if let localResponse = cachedResponseForCurrentRequest(), data = localResponse.data {  
var headerFields:[String : String] = [:]  
headerFields["Content-Length"] = String(format:"%d", data.length)  
if let mimeType = localResponse.mimeType {  
  headerFields["Content-Type"] = mimeType as String  
}  
headerFields["Content-Encoding"] = localResponse.encoding!  
let okResponse = NSHTTPURLResponse(URL: self.request.URL!, statusCode: 200, HTTPVersion: httpVersion, headerFields: headerFields)  
self.client?.URLProtocol(self, didReceiveResponse: okResponse!, cacheStoragePolicy: .NotAllowed)  
self.client?.URLProtocol(self, didLoadData: data)  
self.client?.URLProtocolDidFinishLoading(self)

You may saw that in our code we call methods from client property of NSURLProtocol. That is very important because we want to pass the data and execution flow to the OS and to other objects(example: a web view, a connection, another NSURLSessionDataTask etc.) that made the actual request.

Next we want to implement the methods from NSURLSessionDataDelegate and from NSURLSessionTaskDelegate protocols:

// MARK: NSURLSessionDataDelegate  
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask,  
  didReceiveResponse response: NSURLResponse,  
  completionHandler: (NSURLSessionResponseDisposition) -> Void) {  
  self.client?.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)  
  self.urlResponse = response  
  self.receivedData = NSMutableData()  
  completionHandler(.Allow)  
}  

func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {  
  self.client?.URLProtocol(self, didLoadData: data)  
  self.receivedData?.appendData(data)  
}  

// MARK: NSURLSessionTaskDelegate  
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {  
  if error != nil && error!.code != NSURLErrorCancelled {  
    self.client?.URLProtocol(self, didFailWithError: error!)  
  } else {  
    saveCachedResponse()  
    self.client?.URLProtocolDidFinishLoading(self)  
  }  
}  

For local storage we won’t use CoreData but rather an alternative to it and to SQLite altogether, Realm. I chose this approach because everything that is going on in the custom URL protocol class is being done on a background thread, and for this reason an alternative to CoreData I think is more useful. Because all data transfer within the custom URL protocol class is done in a background thread the UI will not suffer any delay in responsiveness and because of this the user experience will not suffer.

In saveCachedResponse method we verify if there is a local response for the current request, for this we use self.request.URL!.absoluteString!. If we find a local response for current request URL then we will continue with updating that response, if no response is found then we will create a new one. This approach will optimize the records stored locally and remove the redundancy from local storage.

Now that the custom URL protocol class is completed we need to register it. We do this in the first line from method
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool

by calling registerClass method from NSURLProtocol:

NSURLProtocol.registerClass(MyURLProtocol)

Before we run the app we need to tell AFNetworkReachabilityManager that it should start monitoring for network status change, I did this on method:
func applicationDidBecomeActive(application: UIApplication)

by calling startMonitoring method:
AFNetworkReachabilityManager.sharedManager().startMonitoring()

Now we can run the app, and navigate to some pages. After we done this we can stop the internet connection and navigate to the previous pages we went and we can see that they still work.

From this example there are more possibilities we can go. For example we can implement a cache for online cases too, by taking in account the time stamp from local response. The CachedResponse class is very simple:
 import Foundation  
 import Realm  
 class CachedResponse: RLMObject {  
   dynamic var data:NSData!  
   dynamic var encoding:String!  
   dynamic var mimeType:NSString!  
   dynamic var url:String!  
   dynamic var timestamp:NSDate!  
   override init() {  
     super.init()  
     data    = NSData()  
     encoding  = "utf-8"  
     mimeType  = ""  
     url     = ""  
     timestamp  = NSDate()  
   }  
 } 

And there you go, you have implemented a custom NSURLProtocol. The complete project can be downloaded from here. The project has the third party libraries you need, of course you can download new versions of them at any time, links to them are founded below.

The project was created with Xcode 6.4 and Swift 1.2
Useful urls:

I added a new version that was created with Xcode 7 and with Swift 2.0 that you can download from here.

4 comments:

  1. Interested in talking to you about a job, but I can't figure out how to contact you.

    ReplyDelete
  2. Really nice information you had posted. Its very informative and definitely it will be useful for many people
    iOS Training in Chennai
    Android Training in Chennai
    php Training in Chennai

    ReplyDelete
  3. It was so nice article. I was really satisfied by seeing this article. ios online course

    ReplyDelete