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

如何存储视频资源实现复用?解决ScrollView中AVPlayer重复加载问题

解决ScrollView中视频播放器重复加载的问题

SwiftUI的ScrollView子视图滚出可视区域时会被销毁,原代码中VideoPlayerView用@State存储AVPlayer实例,每次视图重建都会重新创建播放器并加载视频,导致重复请求。要解决这个问题,我们需要将播放器状态持久化到视图层级之外,同时支持在视图间传递,最佳方案是使用ObservableObject作为ViewModel管理播放器生命周期。


实现步骤与代码示例

1. 定义播放器状态管理ViewModel

创建一个ViewModel类,持有AVPlayer实例、播放进度、视频宽高比等状态,通过@Published让视图能响应状态变化:

import AVFoundation
import SwiftUI

class VideoPlayerViewModel: ObservableObject {
    @Published var player: AVPlayer?
    @Published var aspectRatio: CGSize = CGSize(width: 16, height: 9)
    private var playbackTime: CMTime = .zero
    private let url: URL
    
    init(url: URL) {
        self.url = url
        setupPlayer()
    }
    
    private func setupPlayer() {
        let player = AVPlayer(url: url)
        // 周期性监听播放进度,保存当前时间
        let timeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: .main) { [weak self] time in
            self?.playbackTime = time
        }
        // 保存观察者避免内存泄漏
        objc_setAssociatedObject(player, &AssociatedKeys.timeObserver, timeObserver, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        self.player = player
        
        // 异步获取视频宽高比
        Task {
            do {
                if let size = try await getVideoResolution(url: url.absoluteString) {
                    await MainActor.run {
                        self.aspectRatio = size
                    }
                }
            } catch {
                print("获取视频分辨率失败: \(error)")
            }
        }
    }
    
    // 恢复播放进度并继续播放
    func resumePlayback() {
        guard let player = player else { return }
        player.seek(to: playbackTime)
        player.play()
    }
    
    // 暂停播放并保存当前进度
    func pausePlayback() {
        player?.pause()
        playbackTime = player?.currentTime() ?? .zero
    }
}

// 用于存储观察者的关联键
private enum AssociatedKeys {
    static var timeObserver = "timeObserver"
}

// 原有的视频分辨率获取函数
func getVideoResolution(url: String) async throws -> CGSize? {
    guard let url = URL(string: url), let track = try await AVURLAsset(url: url).loadTracks(withMediaType: .video).first else {
        return nil
    }
    let naturalSize = try await track.load(.naturalSize)
    let transform = try await track.load(.preferredTransform)
    return naturalSize.applying(transform)
}

2. 修改父视图SomeView

在父视图中创建ViewModel实例,传递给VideoPlayerView,确保ViewModel的生命周期不受子视图销毁影响:

struct SomeView: View {
    let videoUrl = URL(string: "https://drive.google.com/file/d/1fom7lDNjP7thxTuowYtsUXbErvX2jhzH/view?usp=sharing")!
    // 用@StateObject绑定ViewModel,生命周期与父视图一致
    @StateObject private var videoVM = VideoPlayerViewModel(url: URL(string: "https://drive.google.com/file/d/1fom7lDNjP7thxTuowYtsUXbErvX2jhzH/view?usp=sharing")!)
    
    var body: some View {
        ScrollView {
            // 其他视图
            
            VideoPlayerView(vm: videoVM)
            
            // 其他视图
        }
    }
}

3. 重构VideoPlayerView

移除@State变量,直接使用ViewModel的状态,处理视图出现/消失时的播放逻辑:

struct VideoPlayerView: View {
    @ObservedObject var vm: VideoPlayerViewModel
    
    var body: some View {
        ZStack {
            if let player = vm.player {
                VideoPlayer(player: player, showControls: false)
                    .aspectRatio(vm.aspectRatio, contentMode: .fit)
                    .onTapGesture {
                        // 全屏播放:复用同一个player实例,保持进度
                        let fullScreenVC = AVPlayerViewController()
                        fullScreenVC.player = player
                        fullScreenVC.showsPlaybackControls = true
                        // 获取当前窗口并弹出全屏控制器
                        if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
                           let window = windowScene.windows.first {
                            window.rootViewController?.present(fullScreenVC, animated: true)
                        }
                    }
            } else {
                // 加载占位视图
                ProgressView("加载中...")
                    .aspectRatio(vm.aspectRatio, contentMode: .fit)
            }
        }
        .onAppear {
            vm.resumePlayback()
        }
        .onDisappear {
            vm.pausePlayback()
        }
    }
}

4. 调整VideoPlayer UI包装器

简化绑定逻辑,直接接收AVPlayer实例:

struct VideoPlayer: UIViewControllerRepresentable {
    let player: AVPlayer
    let showControls: Bool
    
    func makeUIViewController(context: Context) -> AVPlayerViewController {
        let controller = AVPlayerViewController()
        controller.player = player
        controller.showsPlaybackControls = showControls
        controller.allowsVideoFrameAnalysis = false
        return controller
    }
    
    func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { }
}

核心优化点说明

  • 状态持久化:ViewModel的生命周期由@StateObject绑定到父视图,即使子视图被销毁,ViewModel和AVPlayer实例依然保留,彻底避免重复加载。
  • 进度续播:通过监听播放进度,在视图消失时保存当前位置,返回时自动恢复,提升用户体验。
  • 视图间传递:ViewModel可作为参数在不同视图间传递,比如全屏播放时直接复用同一个AVPlayer实例,保持播放状态一致。
  • 异常处理:添加加载占位视图,异步获取分辨率时处理错误,增强稳定性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 03:37:50