4

I am trying to get PDF loaded in WKWebView directly without making another direct request to URL.

During my research, I came across below link which does the same thing. IOS - How to get cached resources from WKWebView?

but in that approach, I am getting an issue in some of the URLs. I am getting Javascript error in the console Blocked script execution in 'https://ac.els-cdn.com/S0946672X17308763/1-s2.0-S0946672X17308763-main.pdf?_tid=8d087878-4e4c-4c50-b5e4-08ea38906e6e&download=true&acdnat=1534405212_984aaf5f9673f1b2ff107a4032c80ea0' because the document's frame is sandboxed and the 'allow-scripts' permission is not set.

URL to reproduce the issue. https://www.sciencedirect.com/science/article/pii/S0946672X17308763 Just visit in Download PDF link and open the PDF in WKWebView.

EDIT for Ease of loading new Tab windows in same WebView.

-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures{

    if (!navigationAction.targetFrame.isMainFrame) {
        [_webView loadRequest:navigationAction.request];
    }
    return nil;
}

EDIT

I am adding complete swift code derived from the solution provided by @SahilManchanda but it still has some issue with link added in Code snippet.

import UIKit
import WebKit

class ViewController: UIViewController {

    var webView: WKWebView!
    var button: UIButton!
    let link = "https://www.sciencedirect.com/science/article/pii/S0946672X17308763"

    var activityIndicator: UIActivityIndicatorView?
    override func viewDidLoad() {
        super.viewDidLoad()

    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        webView = WKWebView()
        button = UIButton()
        button.setTitle("Save", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.backgroundColor = UIColor.black.withAlphaComponent(0.5)
        button.addTarget(self, action: #selector(self.download(_:)), for: .touchUpInside)
        activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
        webView.navigationDelegate = self
        webView.uiDelegate = self
        activityIndicator?.center = self.view.center
        [webView,button].forEach({view.addSubview($0)})
        setupConstraints()
        webView.configuration.userContentController.add(self, name: "myInterface")
        webView.load(URLRequest(url: URL(string: link)!))
        activityIndicator?.startAnimating()
    }

    func setupConstraints(){
        webView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            webView.topAnchor.constraint(equalTo: view.topAnchor),
            webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
            ])
        button.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -70),
            button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
            button.widthAnchor.constraint(equalToConstant: 100)
            ])
    }
    @objc func download(_ sender: Any){

        let s = """
        var xhr = new XMLHttpRequest();
        xhr.open('GET', "\(webView.url!.absoluteString)", true);
        xhr.responseType = 'arraybuffer';
        xhr.onload = function(e) {
        if (this.status == 200) {
        var uInt8Array = new Uint8Array(this.response);
        var i = uInt8Array.length;
        var binaryString = new Array(i);
        while (i--){
        binaryString[i] = String.fromCharCode(uInt8Array[i]);
        }
        var data = binaryString.join('');
        var base64 = window.btoa(data);

        window.webkit.messageHandlers.myInterface.postMessage(base64);
        }
        };
        xhr.send();
        """
        webView?.evaluateJavaScript(s, completionHandler: {(string,error) in
            print(error ?? "no error")
        })
    }
}
extension ViewController: WKScriptMessageHandler{
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard
            var documentsURL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)).last,
            let convertedData = Data.init(base64Encoded: message.body as! String)
            else {
                //handle error when getting documents URL
                return
        }
        //name your file however you prefer
        documentsURL.appendPathComponent("sample.pdf")
        do {
            try convertedData.write(to: documentsURL)
        } catch {
            //handle write error here
        }
        //if you want to get a quick output of where your
        //file was saved from the simulator on your machine
        //just print the documentsURL and go there in Finder
        print("URL for view \(documentsURL.absoluteString)")
        let activityViewController = UIActivityViewController.init(activityItems: [documentsURL], applicationActivities: nil)
        present(activityViewController, animated: true, completion: nil)
    }
}

extension ViewController: WKNavigationDelegate,WKUIDelegate{

    func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {

        if !(navigationAction.targetFrame != nil && (navigationAction.targetFrame?.isMainFrame)!){
            webView .load(navigationAction.request);
        }

        return nil
    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        self.activityIndicator?.stopAnimating()
        self.activityIndicator?.removeFromSuperview()
        self.activityIndicator = nil
    }
}
Mrug
  • 4,963
  • 2
  • 31
  • 53
  • Clicked Download-> then popup appears -> clicked on download pdf. it didn't opened automatically in wkwebview. however I got the download link in navigationAction delegate e.g. https://www.sciencedirect.com/science/article/pii/S0946672X16303777/pdfft?md5=be79b2d60796a098abf8d79f0b8a8bcf&pid=1-s2.0-S0946672X16303777-main.pdf Can you please guide me how you load the pdf in webview? – Sahil Manchanda Aug 16 '18 at 09:18
  • As per the WKWebview documentation it generally not directly open link target if its set to _blank so, the same thing is happening here. If you want to open thatn you have to make another request in `-(WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures` – Mrug Aug 16 '18 at 09:21
  • Or just use any direct PDF URL from ScienceDirect's website. Its happening for all where some header are there in response from server. e.g. https://www.sciencedirect.com/science/article/pii/S0946672X17308763/pdfft?md5=eafce12181bdd090b19f5dde0885b840&pid=1-s2.0-S0946672X17308763-main.pdf&download=true – Mrug Aug 16 '18 at 09:22
  • Have added code snippet for loading PDF at same place. – Mrug Aug 16 '18 at 09:25
  • Give me a minute. lemme check – Sahil Manchanda Aug 16 '18 at 09:26
  • I tried and got the same error that you are getting. It seems like they have added Content Security Policy sort of thing which blocks the javascript.. – Sahil Manchanda Aug 16 '18 at 09:35
  • Thanks for trying. If you find any solution please share. – Mrug Aug 16 '18 at 09:41
  • I have updated my code and added a button. Now you can save by clicking on button. Make sure you wait for the whole page to get loaded first – Sahil Manchanda Aug 16 '18 at 10:47
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/178124/discussion-between-mrug-and-sahil-manchanda). – Mrug Aug 16 '18 at 11:08

1 Answers1

1

Try Following Code on simulator. The direct pdf links are not available. They generate the pdf link dynamically. So When the page gets finished loading it will have the recent url and calling the download method will request again and thus it will get saved from cache.

   import UIKit
import WebKit

class ViewController: UIViewController {

    var webView: WKWebView!
    var newWebView: WKWebView?
    let link = "https://www.sciencedirect.com/science/article/pii/S0946672X17308763"
    var d = false
    var activityIndicator: UIActivityIndicatorView?
    override func viewDidLoad() {
        super.viewDidLoad()

    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        webView = WKWebView()
        activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
        webView.navigationDelegate = self
        webView.uiDelegate = self
        activityIndicator?.center = self.view.center
        [webView].forEach({view.addSubview($0)})
        setupConstraints()
        webView.configuration.userContentController.add(self, name: "myInterface")
        webView.load(URLRequest(url: URL(string: link)!))
        activityIndicator?.startAnimating()
    }

    func setupConstraints(){
        webView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            webView.topAnchor.constraint(equalTo: view.topAnchor),
            webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
            ])
    }
    func download(_ sender: Any){
        guard let link = (sender as? URL)?.absoluteString else {return}
        print("\(link)")
        let s = """
        var xhr = new XMLHttpRequest();
        xhr.open('GET', "\(link)", true);
        xhr.responseType = 'arraybuffer';
        xhr.onload = function(e) {
        if (this.status == 200) {
        var uInt8Array = new Uint8Array(this.response);
        var i = uInt8Array.length;
        var binaryString = new Array(i);
        while (i--){
        binaryString[i] = String.fromCharCode(uInt8Array[i]);
        }
        var data = binaryString.join('');
        var base64 = window.btoa(data);

        window.webkit.messageHandlers.myInterface.postMessage(base64);
        }
        };
        xhr.send();
        """
        webView?.evaluateJavaScript(s, completionHandler: {(string,error) in
            print(error ?? "no error")
        })
    }
}
extension ViewController: WKScriptMessageHandler{
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        guard
            var documentsURL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)).last,
            let convertedData = Data.init(base64Encoded: message.body as! String)
            else {
                //handle error when getting documents URL
                return
        }
        //name your file however you prefer
        documentsURL.appendPathComponent("sample.pdf")
        do {
            try convertedData.write(to: documentsURL)
        } catch {
            //handle write error here
        }
        //if you want to get a quick output of where your
        //file was saved from the simulator on your machine
        //just print the documentsURL and go there in Finder
        print("URL for view \(documentsURL.absoluteString)")
        let activityViewController = UIActivityViewController.init(activityItems: [documentsURL], applicationActivities: nil)
        present(activityViewController, animated: true, completion: nil)
    }
}

extension ViewController: WKNavigationDelegate,WKUIDelegate{

    func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {

        if !(navigationAction.targetFrame != nil && (navigationAction.targetFrame?.isMainFrame)!){
            webView .load(navigationAction.request);

        }

        return nil
    }

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        self.activityIndicator?.stopAnimating()
        self.activityIndicator?.removeFromSuperview()
        self.activityIndicator = nil
        if d{
            download(webView.url)
        }
    }
    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        print("url \(navigationAction.request.url!.absoluteString)")
        if navigationAction.request.url!.absoluteString.contains("pdfft"){
            if !d{
                d = true
                let url = navigationAction.request.url?.absoluteString.components(separatedBy: "?").first ?? ""
                decisionHandler(.cancel)
                webView.load(URLRequest(url: URL(string: url)!))
                return
            }

        }
        decisionHandler(.allow)
    }
}

After Page finished loading. Click on the save button. It should then print something like below:

URL for view file:///Users/jingged/Library/Developer/CoreSimulator/Devices/B57CEFE8-2A2A-484B-AB36-58E05B68EB1A/data/Containers/Data/Application/17317462-D36C-40EA-AEBD-0F128DC7E66A/Documents/sample.pdf

Copy the url and paste in any browser.

Sahil Manchanda
  • 9,812
  • 4
  • 39
  • 89