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

SwiftUI实现YouTube Shorts预加载:解决TikTok克隆视频切换卡顿

YouTube Shorts预加载优化方案(TikTok克隆应用)

问题描述

我正在用YouTube Shorts开发一款TikTok克隆应用,目前所有功能运行正常,但切换视频时,下一个视频需要数秒才能完成渲染加载。希望实现当前视频播放时预加载下一个视频,使其在切换时即可就绪,求实现思路与建议。

当前代码

import SwiftUI
import WebKit
import UIKit

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).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))
        }
    }
}

struct SingleVideoView: View {
    let link: String
    @State private var viewIsShowing = false
    @State private var isVideoPlaying = false
    var body: some View {
        ZStack {
            Color.black
            
            SmartReelView(link: link, isPlaying: $isVideoPlaying, viewIsShowing: $viewIsShowing)

            Button("", action: {}).disabled(true)
            
            Color.gray.opacity(0.001)
                .onTapGesture {
                    isVideoPlaying.toggle()
                }
            
        }
        .ignoresSafeArea()
        .onDisappear {
            isVideoPlaying = false
            viewIsShowing = false
        }
        .onAppear {
            viewIsShowing = true
            isVideoPlaying = true
        }
    }
}

struct SmartReelView: UIViewRepresentable {
    let link: String
    @Binding var isPlaying: Bool
    @Binding var viewIsShowing: 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) {
        var 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 {
                webView.evaluateJavaScript("clickReady()", 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 = 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)
    }
}

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
    }
}

实现思路与代码修改

核心思路

当前TabView仅在视图即将显示时才初始化SingleVideoView,导致视频加载延迟。解决方案是手动管理预加载队列,在当前视频播放时提前初始化并加载下一个视频的WebView资源,但不播放,切换时直接触发播放。

具体修改步骤

1. 跟踪当前视频索引并维护预加载缓存

在AllVideoView中添加当前索引跟踪和预加载视图缓存,提前创建下一个视频的视图实例:

struct AllVideoView: View {
    @State private var selected = ""
    @State private var arr = ["-q6-DxWZnlQ", "Bp3iu47RRJQ", "lXJdgDjw1Ks", "It3ecCpuzgc", "7WNJjr8QM1w"]
    @State private var currentIndex = 0
    @State private var preloadedViews: [String: SingleVideoView] = [:]
    
    var body: some View {
        ZStack {
            Color.black.edgesIgnoringSafeArea([.bottom, .top])
            TabView(selection: $selected){
                ForEach(arr, id: \.self){ id in
                    preloadedViews[id, default: SingleVideoView(link: id, preload: false)]
                        .tag(id)
                        .onAppear {
                            currentIndex = arr.firstIndex(of: id)!
                            preloadNextVideo()
                        }
                }
                .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 {
                // 初始化时预加载前两个视频
                preloadedViews[arr[0]] = SingleVideoView(link: arr[0], preload: false)
                if arr.count > 1 {
                    preloadedViews[arr[1]] = SingleVideoView(link: arr[1], preload: true)
                }
            }
        }
    }
    
    private func preloadNextVideo() {
        let nextIndex = (currentIndex + 1) % arr.count
        let nextId = arr[nextIndex]
        if preloadedViews[nextId] == nil {
            preloadedViews[nextId] = SingleVideoView(link: nextId, preload: true)
        }
        // 可选:清理已播放过的旧视频缓存,避免内存占用过高
        let prevIndex = (currentIndex - 1 + arr.count) % arr.count
        let prevId = arr[prevIndex]
        if prevId != nextId {
            preloadedViews.removeValue(forKey: prevId)
        }
    }
}

2. 修改SingleVideoView支持预加载模式

添加预加载标志,控制视图加载时机,预加载时仅加载资源不播放:

struct SingleVideoView: View {
    let link: String
    @State private var viewIsShowing = false
    @State private var isVideoPlaying = false
    let preload: Bool
    
    init(link: String, preload: Bool = false) {
        self.link = link
        self.preload = preload
        // 预加载时提前初始化WebView,但不播放
        _viewIsShowing = State(initialValue: preload)
        _isVideoPlaying = State(initialValue: false)
    }
    
    var body: some View {
        ZStack {
            Color.black
            
            SmartReelView(link: link, isPlaying: $isVideoPlaying, viewIsShowing: $viewIsShowing)

            Button("", action: {}).disabled(true)
            
            Color.gray.opacity(0.001)
                .onTapGesture {
                    isVideoPlaying.toggle()
                }
            
        }
        .ignoresSafeArea()
        .onDisappear {
            isVideoPlaying = false
            if !preload {
                viewIsShowing = false
            }
        }
        .onAppear {
            if !preload {
                viewIsShowing = true
                isVideoPlaying = true
            }
        }
    }
}

3. 调整SmartReelView的加载逻辑

修改Coordinator,预加载时仅加载视频资源不自动播放:

class Coordinator: NSObject, WKNavigationDelegate {
    var parent: SmartReelView

    init(_ parent: SmartReelView) {
        self.parent = parent
    }
    
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        guard self.parent.viewIsShowing else { return }
        
        if self.parent.isPlaying {
            webView.evaluateJavaScript("clickReady()", completionHandler: nil)
        } else {
            // 预加载时仅缓存视频资源,不播放
            webView.evaluateJavaScript("player.cueVideoById('\(parent.link)')", completionHandler: nil)
        }
    }
}

4. 优化YouTube播放器参数

在HTML中添加autoplay: 0,避免预加载时自动播放:

player = new YT.Player('player', {
    width: '100%',
    videoId: '\(link)',
    playerVars: { 'playsinline': 1, 'controls': 0, 'autoplay': 0},
    events: {
        'onStateChange': function(event) {
            if (event.data === YT.PlayerState.ENDED) {
                player.seekTo(0);
                player.playVideo();
            }
        }
    }
});

注意事项

  • 控制预加载数量:建议仅预加载当前视频的下一个(或前后各一个),避免内存占用过高。
  • 缓存清理:在切换视频时移除已播放且短时间内不会再次访问的视频视图,释放资源。
  • 网络适配:弱网环境下可适当调整预加载策略,避免影响当前视频播放流畅度。

内容的提问来源于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.11 19:40:54