如何在命令行应用中使用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
相关产品推荐
相关产品推荐

