滚动含YouTube Shorts的TabView时出现错误视频播放问题
解决SwiftUI TabView中YouTube视频播放异常问题
问题根源
- TabView预加载机制:
PageTabViewStyle默认会预加载相邻页面,导致未显示的视图提前触发onAppear,错误设置isPlaying = true。 - WebView回调时机偏差:WKWebView的
didFinish回调可能在视图未真正显示时完成,此时预加载触发的onAppear已将viewIsShowing设为true,导致未显示视频启动播放。 - 状态判断逻辑缺失:原有逻辑仅依赖
onAppear/onDisappear,无法区分视图是预加载状态还是真正处于显示状态。
解决方案
通过TabView的selection状态精确标记当前显示的视频,结合选中状态控制播放逻辑,避免预加载视图误触发播放:
修改后的代码
1. 调整AllVideoView,传递选中状态
struct AllVideoView: View { @State private var selected = "" @State private var arr = ["-q6-DxWZnlQ", "Bp3iu47RRJQ", "lXJdgDjw1Ks", "It3ecCpuzgc", "7WNJjr8QM1w"] var body: some View { ZStack { Color.black.edgesIgnoringSafeArea([.bottom, .top]) TabView(selection: $selected){ ForEach(arr, id: \.self){ id in // 传递当前视图是否被选中的状态 SingleVideoView(link: id, isSelected: selected == id).tag(id) } .rotationEffect(.init(degrees: -90)) .frame(width: widthOrHeight(width: true), height: widthOrHeight(width: false)) } .offset(x: -10.5) .frame(width: widthOrHeight(width: false), height: widthOrHeight(width: true)) .rotationEffect(.init(degrees: 90)) .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) } .onAppear { // 初始化选中第一个视频 if selected.isEmpty, let firstId = arr.first { selected = firstId } } } }
2. 重构SingleVideoView,依赖选中状态控制播放
struct SingleVideoView: View { let link: String let isSelected: Bool @State private var viewIsShowing = false @State private var isVideoPlaying = false var body: some View { ZStack { Color.black SmartReelView(link: link, isPlaying: $isVideoPlaying, viewIsShowing: $viewIsShowing, isSelected: isSelected) Button("", action: {}).disabled(true) Color.gray.opacity(0.001) .onTapGesture { // 仅当前选中的视频允许点击切换播放状态 if isSelected { isVideoPlaying.toggle() } } } .ignoresSafeArea() .onChange(of: isSelected) { newValue in // 监听选中状态变化,控制播放与显示标记 if newValue { viewIsShowing = true isVideoPlaying = true } else { isVideoPlaying = false viewIsShowing = false } } } }
3. 更新SmartReelView,增加选中状态判断
struct SmartReelView: UIViewRepresentable { let link: String @Binding var isPlaying: Bool @Binding var viewIsShowing: Bool let isSelected: 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() webView.configuration.userContentController = userContentController loadInitialContent(in: webView) return webView } func updateUIView(_ uiView: WKWebView, context: Context) { // 非选中视图强制暂停,避免预加载视频播放 guard isSelected else { uiView.evaluateJavaScript("isPlaying = false; watchPlayingState();", completionHandler: nil) return } let jsString = """ isPlaying = \((isPlaying) ? "true" : "false"); watchPlayingState(); """ 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!) { // 仅当前选中且视图显示时,才触发自动播放 if self.parent.viewIsShowing && self.parent.isSelected { webView.evaluateJavaScript("clickReady()", completionHandler: nil) } } } // 原有loadInitialContent方法保持不变 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 = 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 clickReady() { player.playVideo(); } function watchPlayingState() { if (isPlaying) { player.playVideo(); } else { player.pauseVideo(); } } </script> """ webView.scrollView.isScrollEnabled = false webView.loadHTMLString(embedHTML, baseURL: nil) } } // 原有widthOrHeight方法保持不变 func widthOrHeight(width: Bool) -> CGFloat { let scenes = UIApplication.shared.connectedScenes let windowScene = scenes.first as? UIWindowScene let window = windowScene?.windows.first if width { return window?.screen.bounds.width ?? 0 } else { return window?.screen.bounds.height ?? 0 } }
核心修改点
- 用
isSelected状态替代单纯的onAppear/onDisappear,精确识别当前活跃的视频视图 - 在
updateUIView中强制非选中视图暂停,彻底阻断预加载视频的播放可能 - 调整
didFinish回调的触发条件,确保只有当前选中且显示的视频才启动自动播放 - 限制点击暂停/播放的作用范围,仅对当前选中的视频生效
内容的提问来源于stack exchange,提问作者Ahmed Zaidan
相关产品推荐
相关产品推荐

