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
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.