如何在 wkwebview 中下载文件

Posted

技术标签:

【中文标题】如何在 wkwebview 中下载文件【英文标题】:How to download files in wkwebview 【发布时间】:2019-11-28 07:04:00 【问题描述】:

请告诉我如何在 ios wkwebview 中下载文件。我创建了一个 iOS webview 应用程序。在我加载的页面中它有几个下载选项,但是当我点击下载时没有任何反应。

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

【问题讨论】:

看看这个link 【参考方案1】:

自从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`

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

您需要为每次下载分配一个委托,但它可以是所有下载的同一个委托。

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 上受支持。 如前所述,文档仍然稀缺,我只是通过试错找到了如何使其工作,感谢任何反馈。

【讨论】:

WKDownload 委托方法未调用?任何建议 @YogeshPatel 是否调用了 2.2 中提到的任何方法? 是的,我发现它会一直在 .allow 中而不是在 .download 中。我删除 .allow 并添加 .download 然后它称为 WKDownloadDelegate 方法。 使用那么我可以下载音频文件吗? @YogeshPatel 也许 WKWebView 正在尝试播放音频文件而不是下载它。通过将 .allow 替换为 .download,您应该能够下载文件。【参考方案2】:

您也可以使用 javascript 下载文件,正如 Sayooj's link 所暗示的那样。

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

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

然后用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))")  
              
          
      
  

【讨论】:

感谢您的回复。告诉我另外一件事,我应该在此行中提到的 yourframe 中添加什么(webview=wkwebview(frame:yourframe,configuration: webviewconfiguration) view.frame,或view.bounds【参考方案3】:

正如 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()  
          
      

【讨论】:

在实现这个 webview 后只显示白屏@black_pearl 代码都是下载相关的。没有 UI 逻辑。你的问题?请出示一些代码

以上是关于如何在 wkwebview 中下载文件的主要内容,如果未能解决你的问题,请参考以下文章

在 WKWebView swift 中下载文档并加载图像(png、jpeg)、pdf、doc 等

如何在 Puppeteer 中下载文件之前更改文件名?

如何在 iOS 7 中下载文件之前查找文件的大小?

如何使用 webview 组件在 App 中下载文件?

如何使用 Retrofit 库在 Android 中下载文件?

如何在CMD命令中下载文件