如何在 wkwebview 中下载文件
Posted
技术标签:
【中文标题】如何在 wkwebview 中下载文件【英文标题】:How to download files in wkwebview 【发布时间】:2019-11-28 07:04:00 【问题描述】:请告诉我如何在 ios wkwebview 中下载文件。我创建了一个 iOS webview 应用程序。在我加载的页面中它有几个下载选项,但是当我点击下载时没有任何反应。
注意:我不想创建额外的按钮来下载
【问题讨论】:
看看这个link 【参考方案1】:自从macOS 11.3
和iOS 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.3
和 iOS 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 等