如何观测AVPlayerLooper循环次数?Swift/SwiftUI实现及Firestore同步
SwiftUI中AVPlayer循环次数统计与Firestore同步方案
核心结论:观测逻辑放在协调器(Coordinator)中
直接给结论:监听AVPlayer循环次数的逻辑必须放在UIRepresentable的Coordinator里,原因如下:
makeUIView仅在视图首次创建时执行一次,无法响应后续的播放状态变化updateUIView会在SwiftUI视图状态更新时频繁调用,在这里注册监听会导致重复绑定,触发多次无效计数- Coordinator作为SwiftUI和UIKit的中间层,天然适配AVPlayer的生命周期,能安全管理监听事件,不会出现内存泄漏或逻辑混乱
完整实现代码
下面采用分层设计,把数据操作、UI渲染、播放监听拆解开,保证逻辑清晰且计数精准。
1. ViewModel:负责Firestore数据同步
把Firestore的读写逻辑抽离到ViewModel中,避免视图层代码臃肿,同时保证数据操作的可复用性:
import FirebaseFirestore class VideoPlayerViewModel: ObservableObject { private let db = Firestore.firestore() private let videoId: String // 视频唯一标识,用于定位Firestore文档 @Published var loopCount = 0 init(videoId: String) { self.videoId = videoId // 可选:初始化时从Firestore拉取当前循环次数 fetchCurrentLoopCount() } private func fetchCurrentLoopCount() { db.collection("videos").document(videoId).getDocument { [weak self] snapshot, error in guard let data = snapshot?.data(), let count = data["loopCount"] as? Int else { return } DispatchQueue.main.async { self?.loopCount = count } } } func updateLoopCountToFirestore() { // 用Firestore事务保证计数精准,解决多端并发更新的冲突问题 db.runTransaction({ [weak self] transaction, errorPointer -> Any? in guard let self = self else { return nil } let videoRef = self.db.collection("videos").document(self.videoId) guard let snapshot = try? transaction.getDocument(videoRef) else { errorPointer?.pointee = NSError(domain: "FirestoreError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法获取视频文档"]) return nil } let currentCount = snapshot.data()?["loopCount"] as? Int ?? 0 let newCount = currentCount + 1 transaction.updateData(["loopCount": newCount], forDocument: videoRef) return newCount }) { [weak self] result, error in if let error = error { print("更新循环次数失败: \(error.localizedDescription)") } else if let newCount = result as? Int { DispatchQueue.main.async { self?.loopCount = newCount } } } } }
2. UIRepresentable:AVPlayer的SwiftUI包装
负责渲染AVPlayer的UI,Coordinator中处理播放结束的监听:
import SwiftUI import AVKit struct AVPlayerView: UIViewRepresentable { @ObservedObject var viewModel: VideoPlayerViewModel let player: AVPlayer func makeUIView(context: Context) -> UIView { let view = UIView() let playerLayer = AVPlayerLayer(player: player) playerLayer.frame = view.bounds playerLayer.videoGravity = .resizeAspect view.layer.addSublayer(playerLayer) // 给协调器注册播放结束通知 NotificationCenter.default.addObserver(context.coordinator, selector: #selector(Coordinator.handlePlaybackEnd), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem) return view } func updateUIView(_ uiView: UIView, context: Context) { // 仅在player实例变化时更新layer,避免无效操作 if let playerLayer = uiView.layer.sublayers?.first as? AVPlayerLayer, playerLayer.player != player { playerLayer.player = player } } func makeCoordinator() -> Coordinator { Coordinator(viewModel: viewModel, player: player) } class Coordinator: NSObject { private let viewModel: VideoPlayerViewModel private let player: AVPlayer init(viewModel: VideoPlayerViewModel, player: AVPlayer) { self.viewModel = viewModel self.player = player super.init() } @objc func handlePlaybackEnd() { // 手动处理循环:如果actionAtItemEnd是none,说明需要自定义循环逻辑 guard player.actionAtItemEnd == .none else { return } // 重置播放进度到开头并继续播放 player.seek(to: .zero) player.play() // 触发Firestore更新 viewModel.updateLoopCountToFirestore() } deinit { // 移除通知监听,防止内存泄漏 NotificationCenter.default.removeObserver(self) } } }
3. 主视图使用示例
在SwiftUI主视图中整合ViewModel和AVPlayerView:
struct ContentView: View { // 替换为你的实际视频ID和URL private let videoId = "your-video-document-id" private let videoURL = URL(string: "https://example.com/your-video.mp4")! @StateObject private var viewModel: VideoPlayerViewModel init() { _viewModel = StateObject(wrappedValue: VideoPlayerViewModel(videoId: videoId)) // 初始化AVPlayer并设置循环模式 let player = AVPlayer(url: videoURL) player.actionAtItemEnd = .none // 关闭系统默认的播放结束行为,由我们自己处理循环 player.play() } var body: some View { VStack { AVPlayerView(viewModel: viewModel, player: AVPlayer(url: videoURL)) .frame(height: 300) Text("已循环播放: \(viewModel.loopCount) 次") .padding() .font(.headline) } } }
方案合理性说明
- 职责分离:ViewModel管数据,UIRepresentable管UI渲染,Coordinator管播放事件,各层逻辑清晰,易于维护和扩展
- 精准计数:使用Firestore事务更新,避免多端同时更新导致的计数偏差
- 生命周期安全:Coordinator在销毁时自动移除通知监听,不会出现内存泄漏;
makeUIView中仅注册一次监听,不会重复触发计数 - 状态同步:ViewModel的
@Published属性自动驱动SwiftUI视图更新,保证界面显示的循环次数和Firestore数据一致
内容的提问来源于stack exchange,提问作者Harrison Senesac
相关产品推荐
相关产品推荐

