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

如何在命令行应用中使用WKWebView并解决代理方法未调用问题

命令行工具中WKWebView导航代理未触发+自动退出解决方案

问题背景

我要开发一个命令行工具,使用WKWebView截取指定网页的截图,但遇到WKNavigationDelegate方法完全未被调用的问题。代码如下:

import WebKit

class Main: NSObject {
    let webView: WKWebView = WKWebView()
    func load(request: URLRequest) {
        webView.navigationDelegate = self
        webView.load(request)
    }
}

extension Main: WKNavigationDelegate {
    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        print("Did start")
    }
    func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
        print("Did commit")
    }
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        print("Did finish")
    }
    func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        print("Did fail")
    }
}

let main: Main = Main()
let input: String = CommandLine.arguments[1]
if let url: URL = URL(string: input) {
    let request: URLRequest = URLRequest(url: url)
    main.load(request: request)
} else {
    print("Invalid URL")
}

核心疑问:多数WKWebView示例都是在UIKit视图控制器中使用,推测问题源于命令行应用在网页加载完成前就退出,缺少UI应用的事件循环。已知可以用RunLoop.main.run()维持进程,但希望实现类似普通命令行工具的“任务完成即自动退出”效果,比如用async/await替代回调。


解决方案

要点前置

无论哪种方案,必须给WKWebView设置有效frame(非CGRect.zero),否则无法正常渲染页面、触发导航代理,截图也会为空。


方案1:改进RunLoop实现自动退出

直接使用RunLoop.main.run()会让进程无限挂起,我们可以在导航完成(成功/失败)后主动终止RunLoop,实现任务完成即退出:

import WebKit

class Main: NSObject {
    let webView: WKWebView
    private var shouldExit = false

    override init() {
        // 设置网页渲染的尺寸,可根据需求调整
        let frame = CGRect(x: 0, y: 0, width: 1920, height: 1080)
        let config = WKWebViewConfiguration()
        webView = WKWebView(frame: frame, configuration: config)
        super.init()
        webView.navigationDelegate = self
    }

    func loadAndCapture(request: URLRequest) {
        webView.load(request)
        // 循环运行RunLoop,直到导航完成
        while !shouldExit {
            RunLoop.main.run(until: Date().addingTimeInterval(0.1))
        }
    }

    // 截图生成PNG数据
    private func capturePNG() -> Data? {
        guard let snapshotView = webView.snapshotView(afterScreenUpdates: true),
              let cgImage = snapshotView.layer.contents as? CGImage else {
            return nil
        }
        let bitmapRep = NSBitmapImageRep(cgImage: cgImage)
        return bitmapRep.representation(using: .png, properties: [:])
    }
}

extension Main: WKNavigationDelegate {
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        print("网页加载完成")
        // 执行截图并保存
        if let pngData = capturePNG() {
            try? pngData.write(to: URL(fileURLWithPath: "screenshot.png"))
            print("截图已保存到screenshot.png")
        } else {
            print("截图失败")
        }
        // 标记退出RunLoop
        shouldExit = true
    }

    func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
        print("加载失败:\(error.localizedDescription)")
        shouldExit = true
    }

    func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
        print("开始加载网页")
    }
}

// 命令行主逻辑
guard CommandLine.arguments.count > 1 else {
    print("请传入目标URL参数")
    exit(1)
}

let inputURL = CommandLine.arguments[1]
guard let url = URL(string: inputURL) else {
    print("无效的URL格式")
    exit(1)
}

let main = Main()
main.loadAndCapture(request: URLRequest(url: url))

方案2:封装为async/await异步方法

利用Swift的CheckedContinuation将WKWebView的回调式加载流程封装为async方法,配合命令行的异步主函数实现任务完成自动退出:

import WebKit
import Foundation

// 给WKWebView扩展异步加载方法
extension WKWebView {
    func loadAsync(request: URLRequest) async throws {
        try await withCheckedThrowingContinuation { continuation in
            // 临时代理类处理导航回调
            class TempNavDelegate: NSObject, WKNavigationDelegate {
                let continuation: CheckedContinuation<Void, Error>

                init(continuation: CheckedContinuation<Void, Error>) {
                    self.continuation = continuation
                    super.init()
                }

                func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
                    continuation.resume()
                    webView.navigationDelegate = nil // 释放代理避免循环引用
                }

                func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
                    continuation.resume(throwing: error)
                    webView.navigationDelegate = nil
                }

                func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
                    continuation.resume(throwing: error)
                    webView.navigationDelegate = nil
                }
            }

            let delegate = TempNavDelegate(continuation: continuation)
            self.navigationDelegate = delegate
            self.load(request)
        }
    }
}

// 命令行入口(异步主函数)
@main
struct WebScreenshotCLI {
    static func main() async {
        guard CommandLine.arguments.count > 1 else {
            print("用法: \(CommandLine.arguments[0]) <目标URL>")
            exit(1)
        }

        let inputURL = CommandLine.arguments[1]
        guard let url = URL(string: inputURL) else {
            print("无效URL,请检查格式")
            exit(1)
        }

        // 初始化带有效尺寸的WKWebView
        let frame = CGRect(x: 0, y: 0, width: 1920, height: 1080)
        let webView = WKWebView(frame: frame)

        do {
            print("开始加载:\(inputURL)")
            try await webView.loadAsync(request: URLRequest(url: url))
            print("加载完成,生成截图...")

            // 生成并保存截图
            guard let snapshotView = webView.snapshotView(afterScreenUpdates: true),
                  let cgImage = snapshotView.layer.contents as? CGImage else {
                print("截图生成失败")
                exit(1)
            }
            let bitmapRep = NSBitmapImageRep(cgImage: cgImage)
            guard let pngData = bitmapRep.representation(using: .png, properties: [:]) else {
                print("无法转换为PNG格式")
                exit(1)
            }

            try pngData.write(to: URL(fileURLWithPath: "screenshot.png"))
            print("截图已保存到当前目录的screenshot.png")
        } catch {
            print("加载出错:\(error.localizedDescription)")
            exit(1)
        }
    }
}

内容的提问来源于stack exchange,提问作者mingwei

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 12:00:51