SwiftUI自定义视频播放器控件失效及报错问题求助
SwiftUI自定义视频播放器功能修复方案
核心问题诊断
原代码存在4个关键问题导致功能失效和报错:
- 重复创建播放器:
updateUIViewController每次都会重建播放器,状态变更时播放进度、配置全部丢失,控件功能直接失效。 - 状态更新时机错误:在播放状态观察者回调中直接修改
@Binding变量,触发时机与SwiftUI视图更新周期冲突,导致"Can't Publish changes when the view updates"错误。 - 静音功能未正确绑定:仅设置ViewController的静音属性,未同步到底层播放器实例。
- 缺少快进逻辑实现:无任何处理视频跳转的代码。
修复后的完整代码
import Foundation import SwiftUI import AVKit struct UserStories: View { @State private var isPlaying = true @State private var videoDuration: Double = 0 @State private var isMuted = false var body: some View { VStack { LegacyVideoPlayer(url: URL(string: "https://example.com/your-video.mp4")!, // 替换为实际视频URL isPlaying: $isPlaying, videoDuration: $videoDuration, isMuted: $isMuted) HStack(spacing: 20) { Button("播放") { isPlaying = true } Button("暂停") { isPlaying = false } Button("切换静音") { isMuted.toggle() } Button("快进10秒") { NotificationCenter.default.post(name: .fastForwardVideo, object: nil, userInfo: ["seconds": 10.0]) } } .padding() Text("视频时长: \(String(format: "%.1f", videoDuration))秒") } } } // 自定义快进通知 extension Notification.Name { static let fastForwardVideo = Notification.Name("FastForwardVideo") } public struct LegacyVideoPlayer: UIViewControllerRepresentable { let url: URL @Binding var isPlaying: Bool @Binding var videoDuration: Double @Binding var isMuted: Bool public func makeCoordinator() -> CustomPlayerCoordinator { CustomPlayerCoordinator(customPlayer: self) } public func makeUIViewController(context: Context) -> LegacyAVPlayerViewController { let controller = LegacyAVPlayerViewController(videoDuration: $videoDuration, isMuted: $isMuted) controller.delegate = context.coordinator context.coordinator.controller = controller setupAVPlayer(in: controller, context: context) playIfNeeded(controller.player) return controller } public func updateUIViewController(_ uiViewController: LegacyAVPlayerViewController, context: Context) { // 仅同步状态,不重建播放器 playIfNeeded(uiViewController.player) uiViewController.player?.isMuted = isMuted } private func setupAVPlayer(in controller: LegacyAVPlayerViewController, context: Context) { let item = AVPlayerItem(url: url) let player = AVQueuePlayer(playerItem: item) let loopingPlayer = AVPlayerLooper(player: player, templateItem: item) controller.videoGravity = .resizeAspectFill context.coordinator.loopingPlayer = loopingPlayer controller.player = player controller.showsPlaybackControls = false // 监听快进通知 NotificationCenter.default.addObserver(context.coordinator, selector: #selector(CustomPlayerCoordinator.handleFastForward(_:)), name: .fastForwardVideo, object: nil) } private func playIfNeeded(_ player: AVPlayer?) { guard let player = player else { return } if isPlaying && player.rate == 0 { player.play() } else if !isPlaying && player.rate != 0 { player.pause() } } } public class LegacyAVPlayerViewController: AVPlayerViewController { @Binding var videoDuration: Double @Binding var isMuted: Bool private var rateObserver: NSKeyValueObservation? private var itemStatusObserver: NSKeyValueObservation? public override var player: AVPlayer? { willSet { rateObserver?.invalidate() itemStatusObserver?.invalidate() } didSet { setupObservers() } } deinit { rateObserver?.invalidate() itemStatusObserver?.invalidate() } private func setupObservers() { // 监听播放器状态,在readyToPlay时获取视频时长 itemStatusObserver = player?.currentItem?.observe(\.status, options: [.new]) { [weak self] item, _ in guard let self = self else { return } if item.status == .readyToPlay && self.videoDuration == 0 { self.videoDuration = item.duration.seconds } } // 监听播放状态变化(可选,用于同步播放状态到SwiftUI) rateObserver = player?.observe(\.rate, options: [.new]) { [weak self] player, _ in guard let self = self else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.isPlaying = player.rate != 0 } } } public override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) player?.pause() } init(videoDuration: Binding<Double>, isMuted: Binding<Bool>) { _videoDuration = videoDuration _isMuted = isMuted super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } public class CustomPlayerCoordinator: NSObject, AVPlayerViewControllerDelegate { let customPlayer: LegacyVideoPlayer var loopingPlayer: AVPlayerLooper? weak var controller: LegacyAVPlayerViewController? public init(customPlayer: LegacyVideoPlayer) { self.customPlayer = customPlayer super.init() } public func playerViewController(_ playerViewController: AVPlayerViewController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { completionHandler(true) } // 处理快进逻辑 @objc func handleFastForward(_ notification: Notification) { guard let player = controller?.player, let seconds = notification.userInfo?["seconds"] as? Double else { return } let currentTime = player.currentTime().seconds let newTime = CMTimeMakeWithSeconds(currentTime + seconds, preferredTimescale: 1000) player.seek(to: newTime) } }
关键修改说明
1. 避免重复创建播放器
- 将播放器初始化逻辑移到
setupAVPlayer,仅在makeUIViewController中调用一次,updateUIViewController只同步播放/静音状态,不再重建播放器,保证状态不丢失。
2. 修复状态更新报错
- 移除原播放状态回调中直接修改
videoDuration的逻辑,改为监听AVPlayerItem的status属性,在播放器准备就绪时设置时长。 - 使用
DispatchQueue.main.asyncAfter延迟更新状态,避开SwiftUI视图更新周期,解决"Can't Publish changes when the view updates"错误。
3. 完善静音功能
- 在
updateUIViewController中直接设置player?.isMuted = isMuted,确保SwiftUI的状态同步到底层播放器。 - 可选:添加播放状态反向同步,将播放器的实际播放状态同步回SwiftUI的
isPlaying变量。
4. 实现快进功能
- 定义自定义通知
fastForwardVideo,SwiftUI按钮点击时发送快进指令。 - 在Coordinator中实现
handleFastForward方法,通过player.seek(to:)实现视频跳转。
额外优化
- 优化按钮布局,增加间距提升可读性。
- 格式化时长显示,保留一位小数。
- 清理观察者避免内存泄漏。
- 使用弱引用持有ViewController,防止循环引用。
内容的提问来源于stack exchange,提问作者Ahmed Zaidan
相关产品推荐
相关产品推荐

