Custom NSURLProtocol with NSURLSession
Asked Answered
C

4

15

I'm trying to implement this tutorial which implements a custom NSURLProtocol with NSURLConnection.

https://www.raywenderlich.com/76735/using-nsurlprotocol-swift

It works as expected, but now that NSURLConnection is deprecated in iOS9, I'm trying to convert it to NSURLSession.

Unfortunatly it didn't work.

I'm loading a website in uiwebview, if I use NSURLConnection it loads and everything work as expected, all http requests from the webview is captured, but not when using NSURLSession.

Any help is appreciated.

here is my code

    import UIKit

    class MyProtocol: NSURLProtocol, NSURLSessionDataDelegate, NSURLSessionTaskDelegate, NSURLSessionDelegate {

    //var connection: NSURLConnection!
    var mutableData: NSMutableData!
    var response: NSURLResponse!

    var dataSession: NSURLSessionDataTask!

    override class func canInitWithRequest(request: NSURLRequest) -> Bool {

        if NSURLProtocol.propertyForKey("MyURLProtocolHandledKey", inRequest: request) != nil {
            return false
        }

        return true
    }

    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() {
        let newRequest = self.request.mutableCopy() as! NSMutableURLRequest
        NSURLProtocol.setProperty(true, forKey: "MyURLProtocolHandledKey", inRequest: newRequest)

        self.dataSession = NSURLSession.sharedSession().dataTaskWithRequest(newRequest)

        dataSession.resume()
        self.mutableData = NSMutableData()
    }

        override func stopLoading() {

        print("Data task stop")
        self.dataSession.cancel()
        self.mutableData = nil

    }

    func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
        self.response = response
        self.mutableData = NSMutableData()
        print(mutableData)
    }

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

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

    func saveCachedResponse () {
        let timeStamp = NSDate()
        let urlString = self.request.URL?.absoluteString
        let dataString = NSString(data: self.mutableData, encoding: NSUTF8StringEncoding) as NSString?
        print("TiemStamp:\(timeStamp)\nURL: \(urlString)\n\nDATA:\(dataString)\n\n")
    }


    }
Collocate answered 30/3, 2016 at 1:0 Comment(2)
How do you know your code is not working? What have you done to track down the problem yourself? Can you go back through your code example and remove all the commented out sections? Perhaps add some comments about what you are trying to accomplish in each routine.Stafani
Hey, i am new in swift and facing problem with webview caching. I am trying with the same source, webpage load properly and save in cache. But when the device is offline , i can not get it from cache data. May be i have to update the code with urlsession. Can you please help me with this source please :( drive.google.com/file/d/0B-5GPXUpPZh-Q2FOWEJudXRaQkE/…Afterburner
C
16

I've solved it.

Here is the code if anyone needs it.

import Foundation

class MyProtocol1: NSURLProtocol, NSURLSessionDataDelegate, NSURLSessionTaskDelegate
{
private var dataTask:NSURLSessionDataTask?
private var urlResponse:NSURLResponse?
private var receivedData:NSMutableData?

class var CustomKey:String {
    return "myCustomKey"
}

// MARK: NSURLProtocol

override class func canInitWithRequest(request: NSURLRequest) -> Bool {
    if (NSURLProtocol.propertyForKey(MyProtocol1.CustomKey, inRequest: request) != nil) {
        return false
    }

    return true
}

override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
    return request
}

override func startLoading() {

    let newRequest = self.request.mutableCopy() as! NSMutableURLRequest

    NSURLProtocol.setProperty("true", forKey: MyProtocol1.CustomKey, inRequest: newRequest)

    let defaultConfigObj = NSURLSessionConfiguration.defaultSessionConfiguration()
    let defaultSession = NSURLSession(configuration: defaultConfigObj, delegate: self, delegateQueue: nil)

    self.dataTask = defaultSession.dataTaskWithRequest(newRequest)
    self.dataTask!.resume()

}

override func stopLoading() {
    self.dataTask?.cancel()
    self.dataTask       = nil
    self.receivedData   = nil
    self.urlResponse    = nil
}

// 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)
    }
}

// MARK: Private methods

/**
 Do whatever with the data here
 */
func saveCachedResponse () {
    let timeStamp = NSDate()
    let urlString = self.request.URL?.absoluteString
    let dataString = NSString(data: self.receivedData!, encoding: NSUTF8StringEncoding) as NSString?
    print("TimeStamp:\(timeStamp)\nURL: \(urlString)\n\nDATA:\(dataString)\n\n")
}


}
Collocate answered 31/3, 2016 at 6:14 Comment(3)
Thanks man. helped a lot. Below is swift 3 version of same class in case anyone need it.Grape
You really don't need to use a separate NSURLSession for each request. That's grossly inefficient. You should consider using a single shared instance (using a global variable initialized in a dispatch_once block) instead.Preternatural
Is it possible to intercept file request from mobile document directory? It is only intercepting url with either http, https, file:///var/container/... but not file:///var/mobile//Containers/Data/Application/...Laban
G
12

Swift 3 version:

//  CustomURLProtocol.swift

class CustomURLProtocol: URLProtocol, URLSessionDataDelegate, URLSessionTaskDelegate {
  private var dataTask: URLSessionDataTask?
  private var urlResponse: URLResponse?
  private var receivedData: NSMutableData?

  class var CustomHeaderSet: String {
      return "CustomHeaderSet"
  }

  // MARK: NSURLProtocol

  override class func canInit(with request: URLRequest) -> Bool {
      guard let host = request.url?.host, host == "your domain.com" else {
          return false
      }
      if (URLProtocol.property(forKey: CustomURLProtocol.CustomHeaderSet, in: request as URLRequest) != nil) {
          return false
      }

      return true
  }

  override class func canonicalRequest(for request: URLRequest) -> URLRequest {
      return request
  }

  override func startLoading() {

      let mutableRequest =  NSMutableURLRequest.init(url: self.request.url!, cachePolicy: NSURLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval: 240.0)//self.request as! NSMutableURLRequest

      //Add User Agent

      var userAgentValueString = "myApp"
     mutableRequest.setValue(userAgentValueString, forHTTPHeaderField: "User-Agent")

      print(mutableRequest.allHTTPHeaderFields ?? "")
      URLProtocol.setProperty("true", forKey: CustomURLProtocol.CustomHeaderSet, in: mutableRequest)
      let defaultConfigObj = URLSessionConfiguration.default
      let defaultSession = URLSession(configuration: defaultConfigObj, delegate: self, delegateQueue: nil)
      self.dataTask = defaultSession.dataTask(with: mutableRequest as URLRequest)
      self.dataTask!.resume()

  }

  override func stopLoading() {
      self.dataTask?.cancel()
      self.dataTask       = nil
      self.receivedData   = nil
      self.urlResponse    = nil
  }

  // MARK: NSURLSessionDataDelegate

  func urlSession(_ session: URLSession, dataTask: URLSessionDataTask,
                  didReceive response: URLResponse,
                  completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {

      self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)

      self.urlResponse = response
      self.receivedData = NSMutableData()

      completionHandler(.allow)
  }

  func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
      self.client?.urlProtocol(self, didLoad: data as Data)

      self.receivedData?.append(data as Data)
  }

  // MARK: NSURLSessionTaskDelegate

  func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
      if error != nil { //&& error.code != NSURLErrorCancelled {
          self.client?.urlProtocol(self, didFailWithError: error!)
      } else {
          //saveCachedResponse()
          self.client?.urlProtocolDidFinishLoading(self)
      }
  }
}
Grape answered 31/12, 2016 at 2:39 Comment(1)
It's working but showing full website. Dosen't show mobile site view.Breeding
S
10

The problem you are having with your code is that you are using the the NSURLSession.sharedSession to contain your data task. By using the shared session, you are not able to change the session delegate so none of your delegate routines are going to be invoked.

You will need to create a custom session with your protocol established as the delegate for the session. Then, when asked to start loading you can create a data task in that session.

Stafani answered 30/3, 2016 at 4:37 Comment(3)
hi, thanks for reply, the commented out code is for NSURLConnection, as it is deprecated in iOS9 I need to convert it to NSURLSession, What i'm doing is that i want to monitor all http requests from uiwebview and save the JSON response from certain url requests. The code worked fine using NSURLConnection, but now its deprecated. With NSURLConnection I can see the requests and the webview loads the website and any link clicked. but with the change to NSURLSession the webview dosn't load the website.Collocate
Yes I understand. As I said in my answer, when you were using NSURLConnection you set up the delegate for the connection and your delegate methods collected the data. In your new NSSession based code you need to create a custom session instead of using NSSession.sharedSession so that you can set the delegate on that session so your session and data task delegate callbacks can be called.Stafani
Hey, scott thomson , can you please tell me that how i can use the startLoading() method when device is offline to show cache data local backup ? This is the code, i am new in swift and having problem with this. Tut: raywenderlich.com/76735/using-nsurlprotocol-swiftAfterburner
F
2

From the documentation of URLSession:

Important

The session object keeps a strong reference to the delegate until your app exits or explicitly invalidates the session. If you don’t invalidate the session, your app leaks memory until it exits.

Also:

Note

Be careful to not create more sessions than you need. For example, if you have several parts of your app that need a similarly configured session, create one session and share it among them.

So I would move the creation of the URLSession from the startLoading method to the URLProtocol subclass initializer:

class MyURLProtocol: URLProtocol, URLSessionDataDelegate,URLSessionTaskDelegate {
    override init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
        super.init(request: request, cachedResponse: cachedResponse, client: client)
        defaultSession = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
    }
    private var defaultSession: URLSession?
Foulmouthed answered 13/6, 2018 at 0:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.