Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to download files in wkwebview

Anybody please tell me how to download files in iOS wkwebview. I have created an iOS webview app. In the page I have loaded It has several download options but when I click download nothing happened.

Note: I dont want to create extra buttons to download

like image 298
Stephen Balaji Avatar asked Nov 28 '19 07:11

Stephen Balaji


People also ask

How do I use WKWebView?

Here's how: Open the XIB or Storyboard you want to add the web view to in Interface Builder. Find the web view or WKWebView in the Object Library at the bottom-left of Interface Builder. Drag-and-drop a WKWebView object from the Object Library to your view controller's canvas, and adjust its size and position.

How do I load a URL in WKWebView swift 5?

You need to turn the string into a URL , then put the URL into an URLRequest , and WKWebView will load that. Fortunately it's not hard to do! Warning: Your URL must be complete, and valid, in order for this process to work. That means including the https:// part.


Video Answer


2 Answers

Since macOS 11.3 and iOS 14.5 we have finally an API to deal with downloads. But at the time of writing this (June, 2021), documentation is still quite limited: WKDownloadDelegate

1. WKNavigationDelegate

1.1

Add a WKNavigationDelegate to your WKWebView.navigationDelegate

1.2

On your WKNavigationDelegate implement:

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, preferences: WKWebpagePreferences, decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) {
    if navigationAction.shouldPerformDownload {
        decisionHandler(.download, preferences)
    } else {
        decisionHandler(.allow, preferences)
    }
}

This will get called when clicking any link.

navigationAction.shouldPerformDownload will be true when the WKWebView detects the link is meant to download a file.

1.3

Also on your WKNavigationDelegate implement:

func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
    if navigationResponse.canShowMIMEType {
        decisionHandler(.allow)
    } else {
        decisionHandler(.download)
    }
}

This will get called if you answered decisionHandler(.allow, preferences) on the first method, meaning the WKWebView didn't recognise the link as a download, and will try to display it.

navigationResponse.canShowMIMEType will be false if the WKWebView realises it can't display the content.

2. WKDownloadDelegate

2.1

Create a WKDownloadDelegate

2.2

In your WKWebView implement:

func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) {
    download.delegate = // your `WKDownloadDelegate`
}
    
func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) {
    download.delegate = // your `WKDownloadDelegate`
}

One of these methods will be called when you answer .download to any of the methods described on section 1.. The first will be called if it was the first method and second if it was the second method.

You need to assign a delegate to each download, but it can be the same delegate for all of them.

2.3

In your WKDownloadDelegate implement:

func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) {
    let url = // the URL where you want to save the file, optionally appending `suggestedFileName`
    completionHandler(url)
}

This will get called when WKWebView is ready to start a download, but needs a destination URL.

2.4

Optionally, also in your WKDownloadDelegate implement:

func downloadDidFinish(_ download: WKDownload) {
        
}

This will get called when the download finishes.


Final notes

  • Remember both delegates are not retained by the WKWebView, so you need to retain them yourself.
  • There are a few more methods on WKDownloadDelegate useful for dealing with errors, check the documentation for more details (link provided above).
  • Important to remember this is only supported on macOS 11.3 and iOS 14.5.
  • As mentioned before, documentation is still scarce, I just finded how to make this work by trial an error, any feedback appreciated.
like image 109
vauxhall Avatar answered Nov 04 '22 18:11

vauxhall


You can also use JavaScript to download your file, as Sayooj's link implys.

Of cource, you will handle the file downloaded code yourself.

With func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {, you get the file url to download.

Then download it with JS.

the JS call a downloaded method if success, you will be notified with public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {,

Then you can handle your downloaded file

It is a little complicated. Use JavaScript to download file, use WKScriptMessageHandler to communicate between native Swift and JavaScript.

class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler {  

    var webView: WKWebView!  
    let webViewConfiguration = WKWebViewConfiguration()  
    override func viewDidLoad() {  
        super.viewDidLoad()  

        // init this view controller to receive JavaScript callbacks  
        webViewConfiguration.userContentController.add(self, name: "openDocument")  
        webViewConfiguration.userContentController.add(self, name: "jsError")  
        webView = WKWebView(frame: yourFrame, configuration: webViewConfiguration)  
    }  

    func webView(_ webView: WKWebView,  
                 decidePolicyFor navigationAction: WKNavigationAction,  
                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {  
        let url = navigationAction.request.url  
        decisionHandler(.cancel)  
        executeDocumentDownloadScript(forAbsoluteUrl: url!.absoluteString)  

    }  

    /* 
     Handler method for JavaScript calls. 
     Receive JavaScript message with downloaded document 
     */  
    public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {  
        debugPrint("did receive message \(message.name)")  


        if (message.name == "openDocument") {  
            handleDocument(messageBody: message.body as! String)  
        } else if (message.name == "jsError") {  
            debugPrint(message.body as! String)  
        }  
    }  

    /* 
     Open downloaded document in QuickLook preview 
     */  
    private func handleDocument(messageBody: String) {  
        // messageBody is in the format ;data:;base64,  

        // split on the first ";", to reveal the filename  
        let filenameSplits = messageBody.split(separator: ";", maxSplits: 1, omittingEmptySubsequences: false)  

        let filename = String(filenameSplits[0])  

        // split the remaining part on the first ",", to reveal the base64 data  
        let dataSplits = filenameSplits[1].split(separator: ",", maxSplits: 1, omittingEmptySubsequences: false)  

        let data = Data(base64Encoded: String(dataSplits[1]))  

        if (data == nil) {  
            debugPrint("Could not construct data from base64")  
            return  
        }  

        // store the file on disk (.removingPercentEncoding removes possible URL encoded characters like "%20" for blank)  
        let localFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(filename.removingPercentEncoding ?? filename)  

        do {  
            try data!.write(to: localFileURL);  
        } catch {  
            debugPrint(error)  
            return  
        }  

        // and display it in QL  
        DispatchQueue.main.async {  
            // localFileURL  
            // now you have your file
        }  
    }  



    /* 
     Intercept the download of documents in webView, trigger the download in JavaScript and pass the binary file to JavaScript handler in Swift code 
     */  
    private func executeDocumentDownloadScript(forAbsoluteUrl absoluteUrl : String) {  
        // TODO: Add more supported mime-types for missing content-disposition headers  
        webView.evaluateJavaScript("""  
            (async function download() {  
                const url = '\(absoluteUrl)';  
                try {  
                    // we use a second try block here to have more detailed error information  
                    // because of the nature of JS the outer try-catch doesn't know anything where the error happended  
                    let res;  
                    try {  
                        res = await fetch(url, {  
                            credentials: 'include'  
                        });  
                    } catch (err) {  
                        window.webkit.messageHandlers.jsError.postMessage(`fetch threw, error: ${err}, url: ${url}`);  
                        return;  
                    }  
                    if (!res.ok) {  
                        window.webkit.messageHandlers.jsError.postMessage(`Response status was not ok, status: ${res.status}, url: ${url}`);  
                        return;  
                    }  
                    const contentDisp = res.headers.get('content-disposition');  
                    if (contentDisp) {  
                        const match = contentDisp.match(/(^;|)\\s*filename=\\s*(\"([^\"]*)\"|([^;\\s]*))\\s*(;|$)/i);  
                        if (match) {  
                            filename = match[3] || match[4];  
                        } else {  
                            // TODO: we could here guess the filename from the mime-type (e.g. unnamed.pdf for pdfs, or unnamed.tiff for tiffs)  
                            window.webkit.messageHandlers.jsError.postMessage(`content-disposition header could not be matched against regex, content-disposition: ${contentDisp} url: ${url}`);  
                        }  
                    } else {  
                        window.webkit.messageHandlers.jsError.postMessage(`content-disposition header missing, url: ${url}`);  
                        return;  
                    }  
                    if (!filename) {  
                        const contentType = res.headers.get('content-type');  
                        if (contentType) {  
                            if (contentType.indexOf('application/json') === 0) {  
                                filename = 'unnamed.pdf';  
                            } else if (contentType.indexOf('image/tiff') === 0) {  
                                filename = 'unnamed.tiff';  
                            }  
                        }  
                    }  
                    if (!filename) {  
                        window.webkit.messageHandlers.jsError.postMessage(`Could not determine filename from content-disposition nor content-type, content-dispositon: ${contentDispositon}, content-type: ${contentType}, url: ${url}`);  
                    }  
                    let data;  
                    try {  
                        data = await res.blob();  
                    } catch (err) {  
                        window.webkit.messageHandlers.jsError.postMessage(`res.blob() threw, error: ${err}, url: ${url}`);  
                        return;  
                    }  
                    const fr = new FileReader();  
                    fr.onload = () => {  
                        window.webkit.messageHandlers.openDocument.postMessage(`${filename};${fr.result}`)  
                    };  
                    fr.addEventListener('error', (err) => {  
                        window.webkit.messageHandlers.jsError.postMessage(`FileReader threw, error: ${err}`)  
                    })  
                    fr.readAsDataURL(data);  
                } catch (err) {  
                    // TODO: better log the error, currently only TypeError: Type error  
                    window.webkit.messageHandlers.jsError.postMessage(`JSError while downloading document, url: ${url}, err: ${err}`)  
                }  
            })();  
            // null is needed here as this eval returns the last statement and we can't return a promise  
            null;  
        """) { (result, err) in  
            if (err != nil) {  
                debugPrint("JS ERR: \(String(describing: err))")  
            }  
        }  
    }  
}  
like image 26
dengST30 Avatar answered Nov 04 '22 20:11

dengST30