skip to Main Content

I’m trying to override YouTube default play and pause button by disabling the YouTube video and putting a clear view above it that is tappable. When the clear view is tapped it changes a binding which decides if the video will play or not (this video autoplays also).

Currently the isVideoPlaying variable does change correctly and the HTML does pick up on the change. The problem now is that I added an event listener of "onclick" this is never executed. I need to change this event listener to on Change of isVideoPlaying.

But I don’t know how to do this. The only reason tapping the clear view pauses or plays the video is because when the clear view is tapped swift reloads the whole video which makes it go through the ‘onReady’ section. This is another issue, I don’t want the YouTube video to reload every time the clear view is clicked. I want it to simply play and pause without reloading.

For reference I’m trying to make a Tiktok clone using YouTube shorts.

import SwiftUI
import WebKit

struct SingleVideoView: View {
    let link: String
    @State private var isVideoPlaying = true

    var body: some View {
        ZStack {
            SmartReelView(link: link, isPlaying: $isVideoPlaying)

            Button("", action: {}).disabled(true)//to disable YouTube video
            
            Color.gray.opacity(0.001)
                .onTapGesture {
                    isVideoPlaying.toggle()
                }
        }
        .ignoresSafeArea()
    }
}

struct SwiftUIView_Previews: PreviewProvider {
    static var previews: some View {
        SingleVideoView(link: "-q6-DxWZnlQ")
    }
}

struct SmartReelView: UIViewRepresentable {
    let link: String
    @Binding var isPlaying: Bool
    @Environment(.colorScheme) var colorScheme

    func makeUIView(context: Context) -> WKWebView {
        let webConfiguration = WKWebViewConfiguration()
        webConfiguration.allowsInlineMediaPlayback = true
        return WKWebView(frame: .zero, configuration: webConfiguration)
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        let embedHTML = """
        <style>
            .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;
            function onYouTubeIframeAPIReady() {
                player = new YT.Player('player', {
                    width: '100%',
                    videoId: '(link)',
                    playerVars: { 'autoplay': 1, 'playsinline': 1 },
                    events: {
                        'onReady': function(event) {
                            var isPlaying = ((isPlaying) ? "true" : "false");
                            if (isPlaying) {
                                event.target.mute();
                                if (isPlaying) {
                                    event.target.playVideo();
                                }
                            }
                        }
                    }
                });
            }
        
            function watchPlayingState() {
                if (isPlaying) {
                    player.playVideo();
                } else {
                    player.pauseVideo();
                }
            }
            
            document.addEventListener('click', function() {
                watchPlayingState();
            });

        </script>
        """

        uiView.scrollView.isScrollEnabled = false
        uiView.loadHTMLString(embedHTML, baseURL: nil)
    }
}

Update

Following the supply of an answer below, I have modified the code further:

This new code gets the error "Type ‘SmartReelView’ does not conform to protocol ‘UIViewRepresentable’".

struct SmartReelView: UIViewRepresentable {
    let link: String
    @Binding var isPlaying: Bool
    @Environment(.colorScheme) var colorScheme
    var webView: WKWebView?

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, WKNavigationDelegate {
        var parent: SmartReelView
        
        init(_ parent: SmartReelView) {
            self.parent = parent
        }
        
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            parent.webView = webView
        }
    }

    func makeUIView(context: Context) -> WKWebView {
        let webConfiguration = WKWebViewConfiguration()
        webConfiguration.allowsInlineMediaPlayback = true
        let webView = WKWebView(frame: .zero, configuration: webConfiguration)
        
        webView.navigationDelegate = context.coordinator
        return webView
    }

    mutating func updateUIView(_ uiView: WKWebView, context: Context) {
        if webView == nil {
            webView = uiView
            let embedHTML = """
            <style>
                .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: { 'autoplay': 1, 'playsinline': 1 },
                        events: {
                            'onReady': function(event) {
                                event.target.mute();
                                event.target.playVideo();
                            }
                        }
                    });
                }
            
                function watchPlayingState() {
                    if (isPlaying) {
                        player.playVideo();
                    } else {
                        player.pauseVideo();
                    }
                }
            </script>
            """

            uiView.scrollView.isScrollEnabled = false
            uiView.loadHTMLString(embedHTML, baseURL: nil)
        }

        let jsString = "isPlaying = ((isPlaying) ? "true" : "false"); watchPlayingState();"
        webView?.evaluateJavaScript(jsString, completionHandler: nil)
    }
}

2

Answers


  1. Chosen as BEST ANSWER

    To achieve an effect where the video auto plays when it appears and plays and pauses (without restarting video) without reloading when the screen is tapped you can use this code (the correct css has been added for a black background as well):

    struct SmartReelView: UIViewRepresentable {
        let link: String
        @Binding var isPlaying: Bool
        @Environment(.colorScheme) var colorScheme
        
        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: { 'autoplay': 1, 'playsinline': 1 },
                        events: {
                            'onReady': function(event) {
                                event.target.mute();
                                event.target.playVideo();
                            }
                        }
                    });
                }
            
                function watchPlayingState() {
                    if (isPlaying) {
                        player.playVideo();
                    } else {
                        player.pauseVideo();
                    }
                }
            </script>
            """
            
            webView.scrollView.isScrollEnabled = false
            webView.loadHTMLString(embedHTML, baseURL: nil)
        }
    }
    

  2. Your Swift code updates isVideoPlaying, but your JavaScript code does not detect the change. One standard way to communicate between Swift and JavaScript is using WKWebView.evaluateJavaScript. You could use this to run JavaScript code when your isVideoPlaying variable changes.

    And each time you tap the clear view, your video reloads, which is not the behavior you want: you should avoid reloading the entire WKWebView. You can control the video’s play and pause state using YouTube’s JavaScript API.

    So:

    1. Update your SmartReelView to store a reference to WKWebView.
    2. Add a Coordinator class to set up WKNavigationDelegate, as in this thread.
    3. Use evaluateJavaScript to update the isPlaying JavaScript variable when the Swift isVideoPlaying variable changes.
    struct SmartReelView: UIViewRepresentable {
        // (existing code)
        
        // Point 1: Store a reference to WKWebView
        private var webView: WKWebView?
    
        // Point 2: Add a Coordinator class to set up WKNavigationDelegate
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
        
        class Coordinator: NSObject, WKNavigationDelegate {
            var parent: SmartReelView
            
            init(_ parent: SmartReelView) {
                self.parent = parent
            }
            
            // WKNavigationDelegate method to capture the loaded WKWebView
            func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
                parent.webView = webView
            }
        }
    
        func makeUIView(context: Context) -> WKWebView {
            let webConfiguration = WKWebViewConfiguration()
            webConfiguration.allowsInlineMediaPlayback = true
            let webView = WKWebView(frame: .zero, configuration: webConfiguration)
            
            // Set the navigation delegate to your Coordinator
            webView.navigationDelegate = context.coordinator
            return webView
        }
    
        func updateUIView(_ uiView: WKWebView, context: Context) {
            // Only load the HTML when the webView is nil, prevents reloading
            if webView == nil {
                webView = uiView
                // Load HTML here (as in your existing code)
            }
    
            // Point 3: Use evaluateJavaScript to update isPlaying in JavaScript
            let jsString = "isPlaying = ((isPlaying) ? "true" : "false"); watchPlayingState();"
            webView?.evaluateJavaScript(jsString, completionHandler: nil)
        }
    }
    

    In your JavaScript:

    • Remove the var isPlaying = ... line from the onReady function.
    • make sure that watchPlayingState() uses the global isPlaying variable.

    That way, your Swift code directly sets the JavaScript isPlaying variable and calls watchPlayingState() to either play or pause the video accordingly. It does not reload the video, but just uses the YouTube API to control it.

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