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

SwiftUI自定义视频播放器控件失效及报错问题求助

SwiftUI自定义视频播放器功能修复方案

核心问题诊断

原代码存在4个关键问题导致功能失效和报错:

  1. 重复创建播放器:updateUIViewController每次都会重建播放器,状态变更时播放进度、配置全部丢失,控件功能直接失效。
  2. 状态更新时机错误:在播放状态观察者回调中直接修改@Binding变量,触发时机与SwiftUI视图更新周期冲突,导致"Can't Publish changes when the view updates"错误。
  3. 静音功能未正确绑定:仅设置ViewController的静音属性,未同步到底层播放器实例。
  4. 缺少快进逻辑实现:无任何处理视频跳转的代码。

修复后的完整代码

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 22:15:09