'WKWebView - Update HTML Tags from SwiftUI TextFields

In my SwiftUI App, i used a WKWebView to update some html tags from native side, using SwiftUI TextFields and a TextEditor. To get the communication working, i am using the webkit.messageHandlers.bridge.onMessage() to send data from native to webview. Sometime it works and sometimes it doesn't.

My WebView looks like this:

import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    
    @Binding var headline: String
    @Binding var subheadline: String
    @Binding var description: String
    
    class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
        var webView: WKWebView?
        
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            self.webView = webView
        }
        
        func messageToWebview(msg: String) {
            self.webView?.evaluateJavaScript("webkit.messageHandlers.bridge.onMessage('\(msg)')")
        }
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator()
    }
    
    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)
        
        return _wkwebview
    }
    
    func updateUIView(_ webView: WKWebView, context: Context) {
        webView.evaluateJavaScript("webkit.messageHandlers.bridge.onMessage({ type: 'headline', content: '\(headline)' })")
        webView.evaluateJavaScript("webkit.messageHandlers.bridge.onMessage({ type: 'subheadline', content: '\(subheadline)' })")
        webView.evaluateJavaScript("webkit.messageHandlers.bridge.onMessage({ type: 'description', content: '\(description)' })")
    }
}

My index.html code looks like this:

<!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>
<p id="subheadline">Subheadline</p>
<p id="description">Description</p>
<script>
  // to receive messages from native
  webkit.messageHandlers.bridge.onMessage = (data) => {
    if (data.type === 'headline') {
        document.getElementById("headline").textContent = data.content
    }
    if (data.type === 'subheadline') {
        document.getElementById("subheadline").textContent = data.content
    }
    if (data.type === 'description') {
        document.getElementById("description").textContent = data.content
    }
  }
</script>
</body>
</html>

I am using all of that inside my ContentView like this:

import SwiftUI

struct ContentView: View {
    @State private var headline: String = ""
    @State private var subheadline: String = ""
    @State private var description: String = ""
    
    var body: some View {
        NavigationView {
            Form {
                TextField("Your headline", text: $headline)
                TextField("Subheadline", text: $subheadline)
                TextEditor(text: $description)
            }
            WebView(
                headline: $headline,
                subheadline: $subheadline,
                description: $description
            )
        }
    }
}

As you can see, i am using bindings to update the headline, subheadline and my description in realtime. So for e.g when i type inside the headline textfield, the headline will be updated etc.

The problem is that this does not always work. For example, if I open the app and first show and hide the sidebar and then write something in the headline textfield, the h3 does not update. Or when i type something in the headline and in the subheadline textfield and then i want to edit the description, the description will not be updated. It stops working, why?

Where is my mistake and how can I fix this? Any ideas?



Solution 1:[1]

Here is full module of fixed variant that works with Xcode 13.2 / iOS 15.2. Fixed coordinator, of course you should always use it from context, as well several injections and communication between entities.

demo

Note: see also for comments inline

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)')")
    }
}

backup

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1