Search code examples
htmliosswiftyoutubewkwebview

Swift WkMessageHandler message does not send


I'm creating a TikTok clone using YouTube shorts and I would like to get the video length. To do so I have created a function in my embed html that posts a message using WKScriptMessageHandler and the userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage){ function is supposed to pick up this message and update the totalLength variable.

But this currently does not work. The message is never sent and therefore the function above never executes. But the html func sendCurrentTime() does execute when the WKWebView finishes loading. How can I figure out why this message is not sending?

Based on my knowledge the message does not send because the web view is configured incorrectly or html does not have access to the web view object.

struct SmartReelView: UIViewRepresentable {
    let link: String
    @Binding var totalLength: 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()
        userContentController.add(context.coordinator, name: "observe")
        webview.configuration.userContentController = userContentController

        loadInitialContent(web: webview)
        
        return webview
    }
    
    func updateUIView(_ uiView: WKWebView, context: Context) { }
    
    class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
        var parent: SmartReelView

        init(_ parent: SmartReelView) {
            self.parent = parent
        }
        
        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage){
            if message.name == "observe", let Time = message.body as? Double {
                parent.totalLength = Time
            }
        }
        
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            webView.evaluateJavaScript("sendCurrentTime()", completionHandler: nil)
        }
    }
    
    private func loadInitialContent(web: 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 = false;
            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 sendCurrentTime() {
                const length = player.getDuration();
                window.webkit.messageHandlers.observe.postMessage(length);
            }
        </script>
        """
        
        web.scrollView.isScrollEnabled = false
        web.loadHTMLString(embedHTML, baseURL: nil)
    }
}

Solution

  • I did a quick test on my own project. Seems like the sequence of passing the configuration to Webview matters.

    This does not work. Cannot get the callback if the web view is initialized first without setting the userContentController.

    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: "observe")
    webview.configuration.userContentController = userContentController
    

    This works!

    let userContentController = WKUserContentController()
    userContentController.add(context.coordinator, name: "observe")
    
    let webConfiguration = WKWebViewConfiguration()
    webConfiguration.allowsInlineMediaPlayback = true
    webConfiguration.userContentController = userContentController
    
    let webview = WKWebView(frame: .zero, configuration: webConfiguration)
    webview.navigationDelegate = context.coordinator