Home > Back-end >  Change ObservedObject Value with Wkwebview
Change ObservedObject Value with Wkwebview

Time:05-26

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

CodePudding user response:

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

CodePudding user response:

score should be @Binding and updateUIView will be called automatically when it changes. Unfortunately this is one of the many undocumented magic behaviours of SwiftUI's structs.

FYI ObservableObject is designed to assign a Combine pipeline to an @Published. Try and stick to structs for your data and use @State and @Binding to make your value types have reference type semantics.

  • Related