Search code examples
swiftswiftuiwkwebviewcombine

Change ObservedObject Value with Wkwebview


I am using WkWebView for my app. When the view is loaded I would like to display the score Natively and not in the WkWebView. I used the combine Frame work to create an ObservableObject in order to display my score to the view and other views when the score changes in the WkWebview. I use window.onload to get the most recent score and display it when the page first renders. I do so by calling a JS function which sends a message to the Native side webkit.messageHandlers.bridge.postMessage("0") with the score and assign the sent score to my ObservedObject. The issue is on the Native side. The UserContentController function, which handles the message from the WkWebview, keeps printing out the score and reassigning the score to my ObservedObject. It seems to be stuck in a loop. I provided a simplified version of the code below. Have been stuck on this for a few days now and cant seem to fix the issue.

 //Holds the score
class Myscore:ObservableObject{
@Published var score = "0"

}


 //Wkwebview
struct WebView: UIViewRepresentable {
@ObservedObject var myScore : Myscore

 class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
    var webView: WKWebView?
    var myScore: Myscore
    init(myScore:Myscore) {
        self.myScore = myScore
        super.init()
        
    }
    
    
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        self.webView = webView
    }
    
    // receive message from wkwebview
    func userContentController(
        _ userContentController: WKUserContentController,
        didReceive message: WKScriptMessage
    ) {
        // This is where the issue occurs
        var newscore = message.body as! String
        myScore.score = newscore
        print(myScore.score)
    
    }
    
  }

func makeCoordinator() -> Coordinator {
    return Coordinator(myScore:myScore)
}

func makeUIView(context: Context) -> WKWebView {
    let coordinator = makeCoordinator()
    let userContentController = WKUserContentController()
    userContentController.add(coordinator, name: "bridge")
    
    let configuration = WKWebViewConfiguration()
    configuration.userContentController = userContentController
    
    let _wkwebview = WKWebView(frame: .zero, configuration: configuration)
    _wkwebview.navigationDelegate = coordinator
    
    return _wkwebview
}

  func updateUIView(_ webView: WKWebView, context: Context) {
    guard let path: String = Bundle.main.path(forResource: "index", ofType: "html") 
 else { return }
    let localHTMLUrl = URL(fileURLWithPath: path, isDirectory: false)
    webView.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl)
   }
 }

//Content View to display the Score
struct ContentView: View {
 @StateObject var myScore = Myscore()

var body: some View {
    VStack {
     
        Text("Your Score is\( myScore.score)")
        WebView(myScore: myScore)
    }
   }
  }

Edit here is the html side:

<!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>
<button>click me</button>
<hr/>
<div id="log"></div>
<script>
    window.onload = function () {
        webkit.messageHandlers.bridge.postMessage("98 points")

    
    }
 
</script>
</body>
</html>

Edit 2: This is what the console prints out This is what the console prints out


Solution

  • Here I see cycle loading by updating Myscore which result in updateUIView call and so forth...

    Initial loading should be placed into makeUIView

    func makeUIView(context: Context) -> WKWebView {
        let coordinator = makeCoordinator()
        let userContentController = WKUserContentController()
        userContentController.add(coordinator, name: "bridge")
        
        let configuration = WKWebViewConfiguration()
        configuration.userContentController = userContentController
        
        let _wkwebview = WKWebView(frame: .zero, configuration: configuration)
        _wkwebview.navigationDelegate = coordinator
    
        // make here initial loading !!!
        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) {
         // reload should be made here only if base url changed
         // externally
       }
    

    Tested with Xcode 13.4 / iOS 15.5