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
相关产品推荐
相关产品推荐

