You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 12:24:20