如何存储视频资源实现复用?解决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
相关产品推荐
相关产品推荐

