In my SwiftUI App, i used a WKWebView to update some html tags from native side, using SwiftUI TextFields. To get the communication working, i am using the evaluateJavaScript()
method to send data from native to webview.
My code looks like this:
struct DemoView: View {
@State private var headline: String = "Initial"
var body: some View {
NavigationView {
VStack {
Form {
TextField("Your headline", text: $headline)
}
WebView(headline: $headline)
}
}
}
}
import WebKit
let bridgeHTML = """
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, minimum-scale=1, viewport-fit=cover">
</head>
<body>
<h3 id="headline">Headline</h3>
<script>
// to receive messages from native
webkit.messageHandlers.bridge.onMessage = (msg) => {
document.getElementById("headline").textContent = msg
}
</script>
</body>
</html>
"""
struct WebView: UIViewRepresentable {
@Binding var headline: String
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
}
private var owner: WebView
init(owner: WebView) {
self.owner = owner
}
var webView: WKWebView?
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.webView = webView
self.messageToWebview(msg: self.owner.headline) // initial value loading !!
}
func messageToWebview(msg: String) {
self.webView?.evaluateJavaScript("webkit.messageHandlers.bridge.onMessage('\(msg)')")
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(owner: self)
}
func makeUIView(context: Context) -> WKWebView {
let userContentController = WKUserContentController()
userContentController.add(context.coordinator, name: "bridge")
let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController
let _wkwebview = WKWebView(frame: .zero, configuration: configuration)
_wkwebview.navigationDelegate = context.coordinator
guard let path: String = Bundle.main.path(forResource: "index", ofType: "html") else { return _wkwebview }
let localHTMLUrl = URL(fileURLWithPath: path, isDirectory: false)
_wkwebview.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl)
// _wkwebview.loadHTMLString(bridgeHTML, baseURL: nil) // << used for testing
return _wkwebview
}
func updateUIView(_ webView: WKWebView, context: Context) {
// this works for update, but for initial it is too early !!
webView.evaluateJavaScript("webkit.messageHandlers.bridge.onMessage('\(headline)')")
}
}
It works, inside the didFinish()
delegate method i set the initial values to render on the webpage. And when i type something, it automatically updates on the webpage. so far so good, but if I don't do anything for a longer time (about 1 minute) or if I switch the app (not close it) and go back to the app and then change anything in the textfield, the webpage is not updated automatically anymore.
I could of course run webView.reload()
, but then I lose my previous state. Instead of Initial
I get Headline. Logical because this was set inside the h3 tag, but that's what i want to avoid.
So how can i make sure that whenever evaluteJavascript()
is called, the webpage is also updated without losing the previous state, no matter how long i wait or if i switch between apps? Is there any way to solve this?
This looks like Apple's defect, because updateUIView is called, but evaluateJavaScript
(internal JavaScript) generates exception, because bridge.onMessage is lost and not a function more.
Here is found work-around - reset WebView on scene activation (app comes into foreground), as it recreated with current states the previous values are preserved. Tested with Xcode 13.2 / iOS 15.2
struct DemoView: View {
@Environment(\.scenePhase) var scenePhase
@State private var headline: String = "Initial"
@State private var reset = false
var body: some View {
NavigationView {
VStack {
Form {
TextField("Your headline", text: $headline)
}
WebView(headline: $headline).id(reset) // << here !!
.onChange(of: scenePhase) {
if case .active = $0 {
self.reset.toggle() // << here !!
}
}
}
}
}
}