如何在wkwebview中下载文件

时间:2019-11-28 07:04:00

标签: swift wkwebview

任何人都请告诉我如何在iOS wkwebview中下载文件。我创建了一个iOS Webview应用。在该页面中,我已经加载了它有多个下载选项,但是当我单击“下载”时,没有任何反应。

注意:我不想创建额外的按钮来下载

3 个答案:

答案 0 :(得分:1)

Sayooj's link所暗示:

您必须自己处理下载业务

在WKWebView中完成下载任务后,您可以从方法func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {中获取要下载的文件URL

然后您启动下载任务以下载文件,URLSession是一个选择

下载后即可处理文件。上面的链接显示了如何使用QLPreviewController

预览下载的文件
import UIKit    
import WebKit      

class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {    

    var webView: WKWebView!   

    var webViewCookieStore: WKHTTPCookieStore!  
    let webViewConfiguration = WKWebViewConfiguration()  

    override func viewDidLoad() {  
        super.viewDidLoad()  

        webView = WKWebView(frame: yourFrame, configuration: webViewConfiguration)   
        webView.uiDelegate = self  
        webView.navigationDelegate = self  

        view.addSubview(webView)  
        webView.load(URLRequest(url: yourUrlString))    
    }    


    /*  
     Needs to be intercepted here, because I need the suggestedFilename for download  
     */    
    func webView(_ webView: WKWebView,    
                 decidePolicyFor navigationResponse: WKNavigationResponse,    
                 decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {    
        let url = navigationResponse.response.url    

        let documentUrl = url?.appendingPathComponent(navigationResponse.response.suggestedFilename!)    
        loadAndDisplayDocumentFrom(url: documentUrl!)    
        decisionHandler(.cancel)    

    }    

    /*  
     Download the file from the given url and store it locally in the app's temp folder.   
     */    
    private func loadAndDisplayDocumentFrom(url downloadUrl : URL) {  
        let localFileURL = FileManager.default.temporaryDirectory.appendingPathComponent(downloadUrl.lastPathComponent)  

            URLSession.shared.dataTask(with: downloadUrl) { data, response, err in  
                guard let data = data, err == nil else {  
                    debugPrint("Error while downloading document from url=\(downloadUrl.absoluteString): \(err.debugDescription)")  
                    return  
                }  

                if let httpResponse = response as? HTTPURLResponse {  
                    debugPrint("Download http status=\(httpResponse.statusCode)")  
                }  

                // write the downloaded data to a temporary folder  
                do {  
                    try data.write(to: localFileURL, options: .atomic)   // atomic option overwrites it if needed  
                    debugPrint("Stored document from url=\(downloadUrl.absoluteString) in folder=\(localFileURL.absoluteString)")  

                    DispatchQueue.main.async {  
                        // localFileURL  
                        // here is where your file 

                    }  
                } catch {  
                    debugPrint(error)  
                    return  
                }  
            }.resume()  
        }  
    }  

答案 1 :(得分:1)

Sayooj's link暗示,您还可以使用JavaScript下载文件。

当然,您将自己处理文件下载的代码。

使用func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {,您将获得要下载的文件URL。

然后使用JS下载。

如果成功,JS会调用下载的方法,系统会通过public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {通知您,

然后您就可以处理下载的文件

有点复杂。使用JavaScript下载文件,使用WKScriptMessageHandler在本机Swift和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))")  
            }  
        }  
    }  
}  

答案 2 :(得分:0)

自从 macOS 11.3iOS 14.5 以来,我们终于有了处理下载的 API。 但在撰写本文时(2021 年 6 月),文档仍然非常有限:WKDownloadDelegate

1。 WKNavigationDelegate

1.1

WKNavigationDelegate 添加到您的 WKWebView.navigationDelegate

1.2

在您的 WKNavigationDelegate 工具上:

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

点击任何链接时都会调用此方法。

当 WKWebView 检测到该链接用于下载文件时,

navigationAction.shouldPerformDownload 将为真。

1.3

也在您的 WKNavigationDelegate 工具上:

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

如果您在第一种方法中回答 decisionHandler(.allow, preferences),这将被调用,这意味着 WKWebView 没有将该链接识别为下载,并将尝试显示它。

如果 WKWebView 意识到它无法显示内容,

navigationResponse.canShowMIMEType 将为 false。

2. WKDownloadDelegate

2.1

创建一个 WKDownloadDelegate

2.2

在您的 WKWebView 工具中:

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`
}

当您对 .download 部分中描述的任何方法回答 1. 时,将调用其中一个方法。如果是第一种方法,则调用第一个,如果是第二种方法,则调用第二个。

您需要为每个下载分配一个委托,但可以为所有下载指定一个委托。

2.3

在您的 WKDownloadDelegate 工具中:

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

这将在 WKWebView 准备开始下载时调用,但需要目标 URL。

2.4

可选地,也在您的 WKDownloadDelegate 工具中:

func downloadDidFinish(_ download: WKDownload) {
        
}

这将在下载完成时调用。


最后的笔记

  • 请记住,WKWebView 不会保留这两个委托,因此您需要自己保留它们。
  • WKDownloadDelegate 上还有一些有助于处理错误的方法,请查看文档以了解更多详细信息(上面提供的链接)。
  • 重要的是要记住这仅在 macOS 11.3iOS 14.5 上受支持。
  • 如前所述,文档仍然稀缺,我只是发现如何通过试错来完成这项工作,感谢任何反馈。