skip to Main Content

In the iOS WKWebView, is there a way to detect when the website makes a resource request (e.g. an HttpRequest via Ajax)?

In Android, there is a very convenient method "shouldInterceptRequest", but I cannot find an equivalent in iOS.

2

Answers


  1. You can inspect the requests from a WKWebView using the WKNavigationDelegate.
    You may override webView:decidePolicyFor:... to inspect the request, check for its type (e.g. link clicked, form submitted, etc.) and inspect the associated URL to take some action:

    import UIKit
    import WebKit
    
    class ViewController: UIViewController, WKNavigationDelegate {
    
        var webView: WKWebView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            webView = WKWebView(frame: view.bounds)
            webView.navigationDelegate = self
            view.addSubview(webView)
            let url = URL(string: "https://www.google.com")!
            let request = URLRequest(url: url)
            webView.load(request)
        }
    
        // WKNavigationDelegate method to intercept resource requests
        func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    
            // `navigationType` indicates how this request got created
            // `.other` may indicate an Ajax request
            // More info: https://developer.apple.com/documentation/webkit/wknavigationtype
    
            // Update: remove the if clause for now and see if you get any result for your Ajax requests.
            // if navigationAction.navigationType == .other {
                if let request = navigationAction.request.url {
                    print("Resource Request: (request)")
                }
            // }
    
            // Allow the navigation to continue or not, depending on your business logic
            decisionHandler(.allow)
        }
    }
    
    Login or Signup to reply.
  2. As we know we can NOT WKWebViewConfiguration.setURLSchemeHandler with http/https schemes because of the error:

    *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
    reason: ''https' is a URL scheme that WKWebView handles natively'
    

    This happens because WKWebView.handlesURLScheme is called inside and generates this error in case of returned true. Thus we can use an approach by swizzling this method and it allows to handle http/https schemes as well as custom ones.

    func swizzleClassMethod(cls: AnyClass, origSelector: Selector, newSelector: Selector) {
      guard let origMethod = class_getClassMethod(cls, origSelector),
            let newMethod = class_getClassMethod(cls, newSelector),
            let cls = object_getClass(cls)
      else {
        return
      }
      
      if class_addMethod(cls, origSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)) {
        class_replaceMethod(cls, newSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod))
      }
      else {
        method_exchangeImplementations(origMethod, newMethod)
      }
    }
    
    extension WKWebView {
      @objc
      static func _handlesURLScheme(_ urlScheme: String) -> Bool {
        false // Allow all schemes
      }
    }
    
    ...
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
      swizzleClassMethod(cls: WKWebView.self,
                         origSelector: #selector(WKWebView.handlesURLScheme),
                         newSelector: #selector(WKWebView._handlesURLScheme))
    
      return true
    }
    

    Now we can set http/https schemes and intercept all fetch and XMLHttpRequest inside WKWebView, for instance:

    override func viewDidLoad() {
      super.viewDidLoad()
    
      let js = """
      fetch('https://api.github.com/users')
      .then(response => {
        return response.text()
      })
      .then(text => {
        document.getElementById("body").innerHTML += "<br> fetch:" + text;
      })
    
      var ajax = new XMLHttpRequest();
      ajax.onreadystatechange = function() {
        if (this.readyState == 3) {
          document.getElementById("body").innerHTML += "<br> ajax:" + this.responseText;
        }
      };
      ajax.open("GET", "https://api.github.com/users", true);
      ajax.send();
      """
      let script = WKUserScript(source: js, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
      let config = WKWebViewConfiguration()
      config.userContentController.addUserScript(script)
    
      config.setURLSchemeHandler(self, forURLScheme: "https")
    
      webView = WKWebView(frame: view.bounds, configuration: config)
      view.addSubview(webView)
      webView.loadHTMLString("<html><body id='body'></body></html>", baseURL: nil)
    }
    
    extension ViewController: WKURLSchemeHandler {
      func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
        let json = #"[{"login": "mojombo","id": 1}, {"login": "defunkt", "id": 2}]"#
        
        let response = HTTPURLResponse(
          url: urlSchemeTask.request.url!,
          statusCode: 200,
          httpVersion: nil,
          headerFields: [ "access-control-allow-origin": "*"]
        )!
        
        urlSchemeTask.didReceive(response)
        urlSchemeTask.didReceive(json.data(using: .utf8)!)
        urlSchemeTask.didFinish()
      }
      
      func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
      }
    }
    

    Outputs:

    fetch:[{"login": "mojombo","id": 1}, {"login": "defunkt", "id": 2}]
    ajax:[{"login": "mojombo","id": 1}, {"login": "defunkt", "id": 2}]
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search