1

I am trying to find the best place for re-injecting Javascript in the following two contexts:

On-demand injection (only when user presses a button):

Right now, I have the Javascript injection in my WKWebkit updateUIView() function (WKWebkit is wrapped as UIViewRepresentable) but it is being executed too often. (In addition to my state variable change, other circumstances trigger it, like tabbing to a from that view, for instance.) Alternatively, I have tried putting the actual injection code in a Button action. But to do this, I needed access to the webview’s evaluateJavascript() func and the only way I could figure out how to get a handle on the webview was to do a one-time assignment to a variable in makeUIView(). This worked but I got a worrisome warning: “Modifying state during view update, this will cause undefined behavior”. I’m sure there is a better way than either of these two solutions.

Dynamic injections (done dynamically, to show effects in real-time, as a user is altering settings):

Similar to the above, I put the injection in updateUI(). It works but the injection happens more often than desired. Is there some other place for the injection?

Note that both of these scenarios read a local html file. I would like to keep the injections separate from html loading, as it's not necessary in my case.

Here is a simplified, working version of my code, which shows both scenarios. You will need to add the html file (below) to the project.

Many thanks for your advice!

import SwiftUI
import WebKit

struct ContentView: View {
    @State private var webviewNum = 0
    @State private var tmpNum = 0
    
    var body: some View {
        VStack {
            Stepper ("Change webview dynamically: - \(self.webviewNum)", value: self.$webviewNum, in: 0...100)
            
            Divider()
            
            Stepper ("Update webview only when -Inject- pressed:  - \(self.tmpNum)", value: self.$tmpNum, in: 0...100)
            Button(action: {
                self.webviewNum = self.tmpNum
                
                // If I put the Javascript injection here, I need a handle for my webview.
                // How to properly obtain that?
            }) {
                Text("Inject")
            }
            
            Divider()
            
            webView(webviewNum: self.$webviewNum)
        }
    }
}

struct webView: UIViewRepresentable {
    @Binding var webviewNum: Int
    
    func makeUIView(context: Context) -> WKWebView {
        let view = WKWebView()
        let url = Bundle.main.url(forResource: "source", withExtension: "html")
        view.loadFileURL(url!, allowingReadAccessTo: url!.deletingLastPathComponent())
        
        // I could capture a reference to the view here (@Binding var myView = WKWebView)
        // but I get the warning: "Modifying state during view update, this will cause undefined behavior"
        return view
    }
    
    func updateUIView(_ uiView: WKWebView, context: Context) {
        
        print ("updateUIView - inject")
        // Inject the Javascript
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
            let js = "showTotal(\(self.webviewNum));"
            uiView.evaluateJavaScript(js) { (_, error) in
                if error != nil { print("Error: \(String(describing: error))") }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


"source.html":

<html>
<head>
    <title> test </title>
</head>
<body>
    <h1 id="demo"></h1>

    <script>
        function showTotal(num) {
                document.getElementById("demo").innerHTML = 'Via Javascript injection: ' + num ;
        };
    </script>
    
</body>
</html>
matty902
  • 11
  • 2
  • Try to do this in `webView(_ : , didFinish _:)`. See for example https://stackoverflow.com/a/59790493/12299030 – Asperi Jul 30 '20 at 17:02
  • @Asperi Thanks for responding. That is an elegant way of loading the Javascript after the html. However, I am doing multiple injections of the JS (no html reload) via button actions. I think more relevant for my case is another response of yours that I found: https://stackoverflow.com/questions/60971068/swiftui-wkwebview-not-loading. Here, you reload the webview in updateUIView() - so I guess that's not a terrible idea? I still am searching for a better place to re-inject only JS. Maybe the webview being the delegate for the button action? I'm not sure how to do that with a SwiftUI button. – matty902 Aug 02 '20 at 16:47
  • To limit the superfluous Javascript injections in updateUIView(), I have found that in certain situations, turning on a bound boolean flag, "injectJS" and then turning it off after the injection (it must be done async) does work. However, in some cases (like the dynamic stepper in my example), it doesn't. I'm still searching for an ideal implementation... – matty902 Aug 12 '20 at 00:25

0 Answers0