You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift中WKWebView播放YouTube视频时重载而非暂停的问题及代码报错

YouTube Shorts 克隆应用播放/暂停控制问题及修复

问题背景

我正在开发一款模仿TikTok的应用,基于YouTube Shorts实现核心功能。需求如下:

  • 禁用YouTube视频自带的播放/暂停按钮,在视频上方添加一层可点击的透明视图
  • 点击透明视图时切换绑定变量isVideoPlaying,以此控制视频播放状态(视频默认自动播放)
  • 当前问题:isVideoPlaying能正常变化,HTML也能感知到该变量,但添加的onclick事件监听器从未触发。目前点击视图能控制播放/暂停,是因为Swift重新加载了整个视频并执行onReady逻辑,这并非预期效果——我需要的是仅触发播放/暂停动作,而非重载视频。
  • 修改代码后出现新错误:Type 'SmartReelView' does not conform to protocol 'UIViewRepresentable'

初始代码

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 = """\n        <style>\n            .iframe-container iframe {\n                top: 0;\n                left: 0;\n                width: 100%;\n                height: 100%;\n            }\n        </style>\n        <div class="iframe-container">\n            <div id="player"></div>\n        </div>\n        <script>\n            var tag = document.createElement('script');\n            tag.src = "https://www.youtube.com/iframe_api";\n            var firstScriptTag = document.getElementsByTagName('script')[0];\n            firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);\n
            var player;\n            function onYouTubeIframeAPIReady() {\n                player = new YT.Player('player', {\n                    width: '100%',\n                    videoId: '\(link)',\n                    playerVars: { 'autoplay': 1, 'playsinline': 1 },\n                    events: {\n                        'onReady': function(event) {\n                            var isPlaying = \((isPlaying) ? "true" : "false");\n                            if (isPlaying) {\n                                event.target.mute();\n                                if (isPlaying) {\n                                    event.target.playVideo();\n                                }\n                            }\n                        }\n                    }\n                });\n            }\n        \n            function watchPlayingState() {\n                if (isPlaying) {\n                    player.playVideo();\n                } else {\n                    player.pauseVideo();\n                }\n            }\n            \n            document.addEventListener('click', function() {\n                watchPlayingState();\n            });\n
        </script>\n        """

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

更新后的问题与代码

修改代码后出现协议兼容错误,修改后的代码如下:

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 = """\n            <style>\n                .iframe-container iframe {\n                    top: 0;\n                    left: 0;\n                    width: 100%;\n                    height: 100%;\n                }\n            </style>\n            <div class="iframe-container">\n                <div id="player"></div>\n            </div>\n            <script>\n                var tag = document.createElement('script');\n                tag.src = "https://www.youtube.com/iframe_api";\n                var firstScriptTag = document.getElementsByTagName('script')[0];\n                firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);\n
                var player;\n                var isPlaying = true;\n                function onYouTubeIframeAPIReady() {\n                    player = new YT.Player('player', {\n                        width: '100%',\n                        videoId: '\(link)',\n                        playerVars: { 'autoplay': 1, 'playsinline': 1 },\n                        events: {\n                            'onReady': function(event) {\n                                event.target.mute();\n                                event.target.playVideo();\n                            }\n                        }\n                    });\n                }\n            \n                function watchPlayingState() {\n                    if (isPlaying) {\n                        player.playVideo();\n                    } else {\n                        player.pauseVideo();\n                    }\n                }\n            </script>\n            """

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

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

问题修复

1. 解决UIViewRepresentable协议不兼容问题

错误原因是updateUIView方法被标记为mutating,而UIViewRepresentable协议要求该方法不可变。直接去掉mutating关键字即可修复此错误。

2. 优化播放/暂停控制逻辑(避免视频重载)

原实现每次isPlaying变化都会重新加载HTML,导致视频重载。正确做法是:

  • 仅在首次创建WebView时加载HTML内容
  • 通过evaluateJavaScript直接调用YouTube Player API控制播放/暂停,不依赖页面重载

修改后的完整SmartReelView代码:

import SwiftUI
import WebKit

struct SmartReelView: UIViewRepresentable {
    let link: String
    @Binding var isPlaying: Bool
    @Environment(\.colorScheme) var colorScheme
    
    // 通过Coordinator存储WebView引用,符合SwiftUI值类型设计规范
    class Coordinator: NSObject, WKNavigationDelegate {
        var parent: SmartReelView
        var webView: WKWebView?
        
        init(_ parent: SmartReelView) {
            self.parent = parent
        }
        
        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            self.webView = webView
            // 页面加载完成后同步初始播放状态
            parent.syncPlayState(with: self)
        }
    }
    
    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
        webView.scrollView.isScrollEnabled = false
        
        // 首次加载HTML内容
        let embedHTML = """
        <style>
            .iframe-container iframe {
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
            }
            body { margin: 0; padding: 0; }
        </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, 'mute': 1 },
                    events: {
                        'onReady': function(event) {
                            event.target.playVideo();
                        }
                    }
                });
            }
            
            function setPlayState(isPlaying) {
                if (player) {
                    isPlaying ? player.playVideo() : player.pauseVideo();
                }
            }
        </script>
        """
        webView.loadHTMLString(embedHTML, baseURL: nil)
        return webView
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {
        // 每次isPlaying变化时,同步播放状态
        syncPlayState(with: context.coordinator)
    }
    
    private func syncPlayState(with coordinator: Coordinator) {
        let jsString = "setPlayState(\(isPlaying));"
        coordinator.webView?.evaluateJavaScript(jsString) { _, error in
            if let error = error {
                print("执行JavaScript错误: \(error)")
            }
        }
    }
}

3. 简化透明遮罩实现

移除无用的禁用Button,直接用透明Color作为可点击层,确保覆盖整个视频区域:

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

    var body: some View {
        ZStack {
            SmartReelView(link: link, isPlaying: $isVideoPlaying)
            
            // 透明可点击层,拦截底层YouTube的交互
            Color.clear
                .contentShape(Rectangle())
                .onTapGesture {
                    isVideoPlaying.toggle()
                }
        }
        .ignoresSafeArea()
    }
}

关键改进点

  • 移除updateUIView的mutating修饰符,解决协议兼容问题
  • 使用Coordinator存储WebView引用,符合SwiftUI值类型设计规范
  • 仅首次加载HTML内容,后续通过JavaScript调用YouTube Player API控制播放/暂停,彻底避免视频重载
  • 简化透明遮罩实现,用Color.clear配合contentShape确保整个视频区域可点击

内容的提问来源于stack exchange,提问作者Ahmed Zaidan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 00:29:50