0

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?

mingwei
  • 93
  • 6
  • 1
    You can use something like that https://stackoverflow.com/a/28591237/1801544 The idea, is to indeed use the `RunLoop` , but also stops when the work is done. In the `webView(_:didFinish:)` or `webView(_:didFail:withError:)` changing the "stop condition". – Larme Dec 01 '22 at 09:37
  • @Larme thank you, I only saw this after figuring out how to do it with continuations as detailed in [my answer](https://stackoverflow.com/a/75446788/17096282). This would also have worked! – mingwei Feb 14 '23 at 10:54

1 Answers1

0

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.

mingwei
  • 93
  • 6