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>