Hello I would like to change the value of the binding variable ‘click’ once the onReady block in the html is executed. I can communicate from swift to html by using evaluate java script. But how do I communicate from the onReady in the html to swift to change the bools val? I tried making a coordinator class but
struct SmartReelView: UIViewRepresentable {
let link: String
@Binding var isPlaying: Bool
@Binding var click: Bool //variable to be changed
func makeUIView(context: Context) -> WKWebView {
let webConfiguration = WKWebViewConfiguration()
webConfiguration.allowsInlineMediaPlayback = true
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
loadInitialContent(in: webView)
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
let jsString = "isPlaying = ((isPlaying) ? "true" : "false"); watchPlayingState();"
uiView.evaluateJavaScript(jsString, completionHandler: nil)
}
private func loadInitialContent(in webView: WKWebView) {
let embedHTML = """
<style>
body {
margin: 0;
background-color: black;
}
.iframe-container iframe {
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
<div class="iframe-container">
<div id="player"></div>
</div>
<script>
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
var player;
var isPlaying = true;
function onYouTubeIframeAPIReady() {
player = new YT.Player('player', {
width: '100%',
videoId: '(link)',
playerVars: { 'playsinline': 1, 'controls': 0},
events: {
'onReady': function(event) {
//change here
},
'onStateChange': function(event) {
if (event.data === YT.PlayerState.ENDED) {
player.seekTo(0);
player.playVideo();
}
}
}
});
}
function watchPlayingState() {
if (isPlaying) {
player.playVideo();
} else {
player.pauseVideo();
}
}
</script>
"""
webView.scrollView.isScrollEnabled = false
webView.loadHTMLString(embedHTML, baseURL: nil)
}
}
Update
based on the article in the comments I have altered my code:
struct SmartReelView: UIViewRepresentable {
let link: String
@Binding var isPlaying: Bool
@Binding var click: Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let webConfiguration = WKWebViewConfiguration()
webConfiguration.allowsInlineMediaPlayback = true
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.navigationDelegate = context.coordinator
let userContentController = WKUserContentController()
userContentController.add(context.coordinator, name: "toggleMessageHandler")
webView.configuration.userContentController = userContentController
loadInitialContent(in: webView)
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
let jsString = "isPlaying = ((isPlaying) ? "true" : "false"); watchPlayingState();"
uiView.evaluateJavaScript(jsString, completionHandler: nil)
}
class Coordinator: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
var parent: SmartReelView
init(_ parent: SmartReelView) {
self.parent = parent
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "toggleMessageHandler", let messageBody = message.body as? [String: Any] {
if let messageValue = messageBody["message"] as? String, messageValue == "click now" {
DispatchQueue.main.async {
self.parent.click = true
}
}
}
}
}
private func loadInitialContent(in webView: WKWebView) {
let embedHTML = """
<style>
body {
margin: 0;
background-color: black;
}
.iframe-container iframe {
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
<div class="iframe-container">
<div id="player"></div>
</div>
<script>
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
var player;
var isPlaying = true;
function onYouTubeIframeAPIReady() {
player = new YT.Player('player', {
width: '100%',
videoId: '(link)',
playerVars: { 'playsinline': 1, 'controls': 0},
events: {
'onReady': function(event) {
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.toggleMessageHandler) {
window.webkit.messageHandlers.toggleMessageHandler.postMessage({"message": "click now" });
}
},
'onStateChange': function(event) {
if (event.data === YT.PlayerState.ENDED) {
player.seekTo(0);
player.playVideo();
}
}
}
});
}
function watchPlayingState() {
if (isPlaying) {
player.playVideo();
} else {
player.pauseVideo();
}
}
</script>
"""
webView.scrollView.isScrollEnabled = false
webView.loadHTMLString(embedHTML, baseURL: nil)
}
}
2
Answers
To avoid using WKScriptMessageHandler to pass data from the html to swift you can call a function in the embed html that throws and error which is caught by a completion handler in swift. The error object thrown can contain any data needed. Here's an example:
}
To communicate from the embedded HTML to Swift to change a boolean value, you will need to make use of
WKUserContentController
and its delegate methods to listen to custom JavaScript events. You can usewindow.webkit.messageHandlers[handlerName].postMessage(message)
fromWKScriptMessageHandler
in your JavaScript to send messages from JavaScript to Swift (see this question for illustration).The
SmartReelView
needs to create its ownCoordinator
and conform toWKScriptMessageHandler
to handle the JavaScript messages:A
Coordinator
class is introduced that conforms toWKScriptMessageHandler
. That will handle messages from JavaScript.The Coordinator is added to the WebView as a script message handler under the name "toggleMessageHandler".
In your HTML’s
onReady
JavaScript function, a message is posted to this script message handler. Swift’suserContentController(_:didReceive:)
then receives this message, processes it, and updates the@Binding var click
.That should allow the Swift code to react to events in the HTML, changing the
click
variable when theonReady
event fires in JavaScript.That could be from the ordering of operations or potentially a specific environment constraint that is not evident from the provided code. Given that prints inside the
userContentController
method are not showing up, this suggests that the method is never being called. That could be due to JavaScript not successfully sending the message, or because the message handler in Swift is not correctly set up.Make sure the
WKUserContentController
and its message handlers are set before any web content is loaded. You are doing this correctly in yourmakeUIView
function, but it is worth emphasizing.And double-check your JavaScript actually executes the part that posts the message to
window.webkit.messageHandlers.toggleMessageHandler
. You could add console logs to your JavaScript to see if it gets to that point.You have set the
navigationDelegate
to your Coordinator, but make sure you also set theWKScriptMessageHandler
by usinguserContentController.add(context.coordinator, name: "toggleMessageHandler")
.Make sure that
WKUserContentController
and the message handler have been set before loading the web content. The absence of this could lead to a nullwindow.webkit
object. And check whether the WebView is fully initialized when theonReady
function is called. TheWKScriptMessageHandler
might not be fully set up at that point, and hencewindow.webkit
may not be available.You may try invoking
window.webkit.messageHandlers.toggleMessageHandler.postMessage({"message": "click now"});
directly from Safari’s Web Inspector console to manually check if the bridge between JavaScript and Swift works after the webpage is fully loaded. This will at least rule out any issues with the native code and narrow it down to the JavaScript side.Warning messages regarding entitlements like
com.apple.runningboard.assertions.webkit
andcom.apple.multitasking.systemappassertions
usually indicate that the app is trying to execute operations that require special permissions or entitlements. Although these warnings shouldn’t necessarily impact theWKScriptMessageHandler
, make sure you have all the necessary permissions in yourInfo.plist
. See also Where isInfo.plist
in Xcode 13.The behavior you are encountering may very well be due to the restrictions imposed by YouTube’s API and browser security guidelines regarding autoplaying videos with sound. Videos will typically not autoplay with sound due to browser policies that restrict autoplay unless it is muted or triggered by a user action. That is to enhance the user experience and reduce unexpected video playbacks with sound. The YouTube API likely has similar restrictions. Even if you try to play the video using JavaScript in the
onReady
event, the API probably checks whether this request was due to a user action.Given that you are not seeing any web pages loaded when inspecting your device, it does raise questions about whether the
WKUserContentController
and the JavaScript bridge are set up correctly. If the message handler is not added at the right time, it will not be available for JavaScript to call.Your
onAppear
trick might work because SwiftUI sends this after the view has made its appearance, thereby possibly fooling the YouTube API into considering it as a user action.If the goal is to autoplay the video with sound, you might not be able to achieve this without some user action, given the restrictions of web technologies and the YouTube API. However, the approach to use a Swift
@Binding
variable andonChange
to detect when the video is ready to play is a reasonable one. You are essentially trying to create a handshake between the Swift code and the JavaScript code to coordinate the autoplay.Make sure that the
WKUserContentController
andWKScriptMessageHandler
are correctly set up; otherwise, thewindow.webkit
object will not be available in the JavaScript context, and your Swift@Binding
will not receive updates.