如何在macOS的SwiftUI中通过WKWebView下载文件
解决SwiftUI中WKWebView在macOS上无法下载.doc文件的问题
核心修改思路
WKWebView默认不会自动处理data: URI类型的文件下载请求,需要让协调器(Coordinator)同时遵守WKUIDelegate协议,实现文件保存面板相关方法,手动完成下载流程。
具体代码修改
1. 更新WebViewCoordinator的协议声明
修改WebViewCoordinator,添加WKUIDelegate协议遵守:
@available(OSX 11.0, *) class WebViewCoordinator: NSObject, WKNavigationDelegate, WKUIDelegate { // 原有代码保持不变 }
2. 设置UIDelegate
在WebViewCoordinator的初始化方法中,添加webView.uiDelegate = self:
init(data: WebViewData) { self.data = data super.init() webView.navigationDelegate = self webView.uiDelegate = self // 新增该行,绑定UIDelegate }
3. 实现下载处理相关方法
在WebViewCoordinator中新增以下方法,拦截并处理下载请求:
// 处理系统保存面板 func webView(_ webView: WKWebView, runOpenPanelWith parameters: WKOpenPanelParameters, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping ([URL]?) -> Void) { guard let suggestedFilename = parameters.suggestedFilename else { completionHandler(nil) return } let savePanel = NSSavePanel() savePanel.title = "保存文档" savePanel.nameFieldStringValue = suggestedFilename savePanel.allowedFileTypes = ["doc"] savePanel.begin { response in completionHandler(response == .OK ? [savePanel.url!] : nil) } } // 拦截data URI请求并处理保存 func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { guard let url = navigationAction.request.url, url.scheme == "data" else { decisionHandler(.allow) return } guard let fileData = dataFromDataURI(url.absoluteString) else { showError(title: "解析失败", message: "无法处理该下载链接") decisionHandler(.cancel) return } let savePanel = NSSavePanel() savePanel.title = "保存Word文档" savePanel.nameFieldStringValue = "文档.doc" savePanel.allowedFileTypes = ["doc"] savePanel.begin { response in if response == .OK, let saveUrl = savePanel.url { do { try fileData.write(to: saveUrl) let successAlert = NSAlert() successAlert.messageText = "保存成功" successAlert.informativeText = "文件已成功保存到指定路径" successAlert.alertStyle = .informational successAlert.runModal() } catch { self.showError(title: "保存失败", message: error.localizedDescription) } } } decisionHandler(.cancel) } // 辅助方法:解析data URI为Data对象 private func dataFromDataURI(_ uri: String) -> Data? { let parts = uri.components(separatedBy: ",") guard parts.count >= 2 else { return nil } let contentPart = parts[1] if uri.lowercased().contains("base64") { return Data(base64Encoded: contentPart) } else { guard let decodedContent = contentPart.removingPercentEncoding else { return nil } return decodedContent.data(using: .utf8) } }
4. 完整的WebViewCoordinator代码
更新后的完整代码如下:
@available(OSX 11.0, *) class WebViewCoordinator: NSObject, WKNavigationDelegate, WKUIDelegate { @ObservedObject var data: WebViewData var webView: WKWebView = WKWebView() var loadedUrl: URL? = nil init(data: WebViewData) { self.data = data super.init() webView.navigationDelegate = self webView.uiDelegate = self } // 原有导航状态监听方法保持不变 func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { DispatchQueue.main.async { self.data.loading = false } } func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { DispatchQueue.main.async { self.data.loading = true } } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { showError(title: "导航错误", message: error.localizedDescription) DispatchQueue.main.async { self.data.loading = false } } func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { showError(title: "加载错误", message: error.localizedDescription) DispatchQueue.main.async { self.data.loading = false } } func showError(title: String, message: String) { #if os(macOS) let alert: NSAlert = NSAlert() alert.messageText = title alert.informativeText = message alert.alertStyle = .warning alert.runModal() #else print("\(title): \(message)") #endif } // 新增下载处理方法 func webView(_ webView: WKWebView, runOpenPanelWith parameters: WKOpenPanelParameters, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping ([URL]?) -> Void) { guard let suggestedFilename = parameters.suggestedFilename else { completionHandler(nil) return } let savePanel = NSSavePanel() savePanel.title = "保存文档" savePanel.nameFieldStringValue = suggestedFilename savePanel.allowedFileTypes = ["doc"] savePanel.begin { response in completionHandler(response == .OK ? [savePanel.url!] : nil) } } func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { guard let url = navigationAction.request.url, url.scheme == "data" else { decisionHandler(.allow) return } guard let fileData = dataFromDataURI(url.absoluteString) else { showError(title: "解析失败", message: "无法处理该下载链接") decisionHandler(.cancel) return } let savePanel = NSSavePanel() savePanel.title = "保存Word文档" savePanel.nameFieldStringValue = "文档.doc" savePanel.allowedFileTypes = ["doc"] savePanel.begin { response in if response == .OK, let saveUrl = savePanel.url { do { try fileData.write(to: saveUrl) let successAlert = NSAlert() successAlert.messageText = "保存成功" successAlert.informativeText = "文件已成功保存到指定路径" successAlert.alertStyle = .informational successAlert.runModal() } catch { self.showError(title: "保存失败", message: error.localizedDescription) } } } decisionHandler(.cancel) } private func dataFromDataURI(_ uri: String) -> Data? { let parts = uri.components(separatedBy: ",") guard parts.count >= 2 else { return nil } let contentPart = parts[1] if uri.lowercased().contains("base64") { return Data(base64Encoded: contentPart) } else { guard let decodedContent = contentPart.removingPercentEncoding else { return nil } return decodedContent.data(using: .utf8) } } }
代码说明
- 通过绑定
WKUIDelegate,获得WKWebView的文件交互处理权限。 - 拦截
data:协议的请求,解析其中的文件内容。 - 调用系统原生
NSSavePanel让用户选择保存路径,完成文件写入。
内容的提问来源于stack exchange,提问作者alex
相关产品推荐
相关产品推荐

