skip to Main Content

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


  1. Chosen as BEST ANSWER

    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:

    struct SmartReelView: UIViewRepresentable {
    let link: String
    @Binding var isPlaying: Bool
    @Binding var click: Bool
    @Binding var totalLength: Double
    @Binding var currentTime: Double
    
    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()
        
        webView.configuration.userContentController = userContentController
    
        loadInitialContent(in: webView)
        
        return webView
    }
    
    func updateUIView(_ uiView: WKWebView, context: Context) {
        let jsString = """
                isPlaying = ((isPlaying || click) ? "true" : "false");
                watchPlayingState();
                seekToTime((currentTime));
            """
        uiView.evaluateJavaScript(jsString, completionHandler: nil)
    }
    
    class Coordinator: NSObject, WKNavigationDelegate {
        var parent: SmartReelView
    
        init(_ parent: SmartReelView) {
            self.parent = parent
        }
        
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            webView.evaluateJavaScript("getLength()", completionHandler: { (result, error) in
                if let error = error {
                    let string = "(error)"
                    do {
                        let regex = try NSRegularExpression(pattern: "Video duration: (\d+)", options: [])
                        if let match = regex.firstMatch(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count)) {
                            if let range = Range(match.range(at: 1), in: string) {
                                let videoDuration = string[range]
                                self.parent.totalLength = Double(videoDuration) ?? 0.0
                            }
                        }
                    } catch { print("") }
                }
            })
        }
    }
    
    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: {
                        'onStateChange': function(event) {
                            if (event.data === YT.PlayerState.ENDED) {
                                player.seekTo(0);
                                player.playVideo();
                            }
                        }
                    }
                });
            }
            
            function watchPlayingState() {
                if (isPlaying) {
                    player.playVideo();
                } else {
                    player.pauseVideo();
                }
            }
        
            function sendCurrentTime() {
                const currentTime = player.getCurrentTime();
            }
        
            function seekToTime(seconds) {
                if (player && seconds > 0.0) {
                    player.seekTo(seconds);
                    timeToSeek = 0.0;
                }
            }
    
            function getLength() {
                var length = player.getDuration();
                var errorMessage = "Video duration: " + length;
                throw new Error(errorMessage);
            }
        </script>
        """
        
        webView.scrollView.isScrollEnabled = false
        webView.loadHTMLString(embedHTML, baseURL: nil)
    }
    

    }


  2. 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 use window.webkit.messageHandlers[handlerName].postMessage(message) from WKScriptMessageHandler in your JavaScript to send messages from JavaScript to Swift (see this question for illustration).

    The SmartReelView needs to create its own Coordinator and conform to WKScriptMessageHandler to handle the JavaScript messages:

    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
            
            // Create user content controller
            let userContentController = WKUserContentController()
            
            // Add the message handler script
            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
                        }
                    }
                }
            }
        }
        
        // existing loadInitialContent function
    }
    

    A Coordinator class is introduced that conforms to WKScriptMessageHandler. 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’s userContentController(_: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 the onReady event fires in JavaScript.


    It seems like the message is never received. Adding prints at the start of "userContentController" never run.

    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 your makeUIView 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 the WKScriptMessageHandler by using userContentController.add(context.coordinator, name: "toggleMessageHandler").


    The onReady block does execute for sure but the ".postMessage({"message": "click now" });" is never reached, meaning the if statements before it are evaluated to false. Removing the if statements still does not trigger the message. This means window.webkit is nil.

    I also tried running this on a real device and that did not help. The only other thing is a warning I get every time the video is played and paused which is: "originator doesn't have entitlement com.apple.runningboard.assertions.webkit AND originator doesn't have entitlement com.apple.multitasking.systemappassertions"

    Make sure that WKUserContentController and the message handler have been set before loading the web content. The absence of this could lead to a null window.webkit object. And check whether the WebView is fully initialized when the onReady function is called. The WKScriptMessageHandler might not be fully set up at that point, and hence window.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 and com.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 the WKScriptMessageHandler, make sure you have all the necessary permissions in your Info.plist. See also Where is Info.plist in Xcode 13.


    I think WKUserContentController is just not set up right. When I plug in my iPhone to and run the app, go to the YouTube video on my Mac and press on "Develop" then hover over my iPhone I don’t see any web pages loaded. Taking a step back the only reason I want to change a bool value because if I do so I can listen for changes to the var and then in the onChange block I can set the isPlaying variable to true which will go back and unpause the video. This is all just so the video plays (UNMUTED) when it’s ready. For some reason calling watchPlayingState from onReady doesn’t work.

    I think the YouTube AGO goes through the js and makes sure the pause action is stimulated by a physical user action? How else would it know not to play the video. Calling watchPlayingState in the onReady doesn’t work (but muting the vid right before does autoplay vid). But if I put a simple onAppear in my main view and toggle isPlaying then the video auto plays with sound (maybe API is tricked to think a user caused it). All the onAppear really does is make watchPlayingState run.

    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 and onChange 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 and WKScriptMessageHandler are correctly set up; otherwise, the window.webkit object will not be available in the JavaScript context, and your Swift @Binding will not receive updates.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search