I'm trying to write a command line tool that takes a screenshot of a given webpage using WKWebView. The problem is that WKNavigationDelegate methods aren't being called. This is what I have:
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")
}
Almost all examples I've found involve using WKWebView in a view controller. My guess is that in the command line, the app exits before the webpage finishes loading, but I'm not sure how to prevent that from happening.
I did find this example of a command line tool using WKWebView. The author uses RunLoop.main.run(), which to my understanding effectively simulates the event loop of a UI app? That allows the webpage to load, but I'm looking for a different solution because I want the app to behave like a normal command line tool and exit on its own after running. For example, is there some way to use async/await with WKWebView.load() much like with URLSession?
I ended up solving this problem using continuation
. In short, I wrap webView.load()
in a continuation
and then call continuation.resume()
in one of the WKNavigationDelegate
methods. That allows me to treat webView.load()
as an async task; the continuation determines its runtime. I took this solution entirely from the example in this blog post.
Here's a barebones implementation of this solution:
import WebKit
@MainActor
class WebContainer: NSObject {
lazy var webView: WKWebView = {
let webView: WKWebView = WKWebView()
webView.navigationDelegate = self
return webView
}()
var continuation: UnsafeContinuation<Void, Error>?
func load(request: URLRequest) async throws -> Int {
try await withUnsafeThrowingContinuation { continuation in
self.continuation = continuation
webView.load(request)
}
}
}
extension WebContainer: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
continuation?.resume(returning: ())
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
continuation?.resume(throwing: error)
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
continuation?.resume(throwing: error)
}
}
To load a website using the above implementation, simply run:
let webContainer: WebContainer = WebContainer()
let request: URLRequest = // insert URLRequest here
try await webContainer.load(request: request)
Practically speaking this implementation doesn't handle redirects reliably as some redirects are initiated after didFinish is called. I have a partial solution to that which however runs into other problems. Since all this is out of scope for this question, if anyone's interested please refer to this other question.