WKWebView CSS does not update (MacOS, SwiftUI)

Folks,

I’m writing a private app to browse through HTML contents stored on a website using that website's API. Everything works perfectly, except when I want to add a custom CSS to pretty print the contents.

I’m using a WKWebView suitably piggybacked to a SwiftUI view, and tried and use a small JS script to inject the CSS into the loaded page. The method I found makes use of WKUserContentController and creates a user script which is (purportedly) loaded after the page is rendered.

But that doesn’t work. The JS script is injected alright, but the view does not update. Worse, if I select another HTML chunk from the list of available pages, the chunk loads fine but the CSS is not applied. So the view does update, but spurns the script.

Yet, if I switch to an HTML source view, which kills the WKWebView instance, and then bring it back in, the CSS miraculously activates.

So, am I wrong, or shall I deduce that WKUserContentController's scripts are executed only once, when the view appears? What's more puzzling is that the updateNSView method of the wrapping view (which itself calls loadHTMLString) is correctly called after the CSS has been injected, so that should result in a view update, but it doesn’t.

What gives?

For reference, this is the piggybacking object:

struct WebView: NSViewRepresentable {
    
    typealias NSViewType = WKWebView
    
    let webView: WKWebView
    let wkUCC = WKUserContentController()
    let wkConfig = WKWebViewConfiguration()
    var HTML: String?
    var zoom: Double
    var CSS: String?
    
    init(HTML: String?, CSS: String?, zoom: Double) {
        wkConfig.userContentController = wkUCC
        self.webView = WKWebView(frame: CGRect.zero, configuration: wkConfig)
        self.HTML = HTML
        self.zoom = zoom
        self.CSS = CSS
        if let css = CSS {
            loadCSS(css: css)
        }
    }
    
    func loadCSS (css: String) {
        
        let script = """
        var oldStyles = document.getElementsByTagName('style');
        if (oldStyles != null) {
            for (let oldStyle of oldStyles) {
                document.head.removeChild(oldStyle);
            }
        }
        var newStyle = document.createElement('style');
        newStyle.innerHTML = '\(css)';
        var result = document.head.appendChild(newStyle);
        """
            .replacing(/\n/, with: "")
        let userScript = WKUserScript(source: script,
                                      injectionTime: .atDocumentEnd,
                                      forMainFrameOnly: true)
        wkUCC.removeAllUserScripts()
        wkUCC.addUserScript(userScript)
    }
    
    func makeNSView(context: Context) -> WKWebView {
        return webView
    }
    
    
    func updateNSView(_ webView: WKWebView, context: Context) {
        if var HTML = self.HTML {
            webView.loadHTMLString(HTML, baseURL: nil)
            webView.pageZoom = zoom
        }
    }
}

and the calling code in the wrapping view:

if displaySource {
                    TextEditor(text: Binding<String>(get: {item.HTML}, set:{_ in}))
                        .font(.system(size: zoom * 14, weight: .light, design: .monospaced))
                        .padding(.horizontal, 5)
                } else {
                    WebView(HTML: item.HTML, CSS: CSS, zoom: zoom)
                        .padding(.horizontal, 5)
                }

P.S: I tried to use various decorators such as @State and @Biding to shake things up, but to no avail.

  • Probably try injecting JS as well to reload the page.

Add a Comment

Replies

I've tried that, it doesn’t work. reload () as well as reloadFromOrigin () result in in blank pages. Probably because there is no URL (The data arrives using the API, then is stored as a String).

Meanwhile, I have dug a bit into it using Instruments.

While the first apparition of the WebView takes around 11.2 ms, the update triggered by the JS injection lasts only 3.3 ms. So clearly, the HTML page is not parsed again the second time. Apparently the code calls a method of a WebKit::WebPageProxy object, so I wonder if some form of caching doesn’t interfere at this point, though later (and right before the update finishes), the code calls CSSStore2::String::Find. But at no point do I see any hint of JS script execution.

As a further note, it just occurred to me that my custom WebView does not appear in Instruments under the SwiftUI list of managed views. Would that mean it receives a different treatment than standard views?

In any case, I think it is high time Apple added a built-in web browser to the collection of SwiftUI gadgets.

  • To be honest and complete, since the HTML code I display has no header, I worked out a simple kludge which prepends the CSS portion (with suitable <head><style> tags) to the HTML string.

Add a Comment