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

iOS 17 Beta中App与Widget共享AVAudioPlayer状态的实现方案

如何在主应用与Widget扩展间共享AVAudioPlayer状态?

核心问题原因

主应用和Widget扩展是独立的进程,你当前使用的单例PlayerModel.shared在两个进程中是完全不同的实例,因此状态无法自动同步。要实现类似Apple Music的实时状态同步,必须依赖跨进程的状态共享机制。

可行解决方案

1. 基于系统Media Player框架实现状态同步

利用MPNowPlayingInfoCenter和MPRemoteCommandCenter是最直接的方案,这两个组件是系统级别的,天然支持跨进程状态共享:

  • 主应用在播放状态变化时(播放/暂停、进度更新),更新MPNowPlayingInfoCenter.default().nowPlayingInfo,把播放状态、进度、曲目信息写入其中。
  • Widget通过在Timeline刷新时读取当前的nowPlayingInfo,更新Widget的UI(比如播放按钮状态、进度条)。
  • 所有播放控制操作(播放/暂停)都通过AudioPlaybackIntent触发,由主应用的PlayerModel处理,确保只有一个播放器实例在运行。

2. 利用App Groups共享状态数据

通过App Groups实现主应用和Widget之间的数据共享,补充Media Player框架的不足:

  • 在开发者后台为应用和Widget配置相同的App Group。
  • 使用UserDefaults(suiteName: "your.app.group.id")存储播放状态(如isPlaying、currentTrackURL、currentProgress等)。
  • 主应用每次更新播放状态时,同步写入共享的UserDefaults。
  • Widget的TimelineProvider在刷新时读取共享UserDefaults的数据,生成对应的TimelineEntry,更新Widget UI;也可以通过NotificationCenter监听UserDefaults.didChangeNotification触发状态更新。

3. 结合WidgetKit的Timeline刷新机制

Widget的UI更新依赖Timeline,你可以:

  • 在主应用状态变化时,调用WidgetCenter.shared.reloadAllTimelines()主动触发Widget刷新。
  • 在Widget的TimelineProvider中设置合理的刷新间隔,确保状态能及时同步。

Apple Music的实现方案

Apple Music作为系统级应用,主要依赖:

  • 系统原生的Media Player框架(MPNowPlayingInfoCenter、MPRemoteCommandCenter)实现跨进程状态同步,系统会自动维护播放状态的一致性。
  • 底层的音频会话管理,系统级应用拥有更高的权限来控制音频播放状态,确保Widget和主应用的状态实时同步。第三方应用无法使用系统私有API,因此只能基于公开的Media Player框架+App Groups实现类似效果。

代码修改示例

1. 改造PlayerModel,增加App Groups状态同步

import WidgetKit

class PlayerModel: ObservableObject {
    static let shared = PlayerModel()
    @Published var mainPlayer: AVPlayer?
    @Published var isPlaying: Bool = false
    // 共享的UserDefaults,替换为你的App Group ID
    private let sharedDefaults = UserDefaults(suiteName: "com.your.app.group")
    
    private var timeObserver: Any?

    private init() {
        setupCommandCenter()
        // 从共享存储恢复初始状态
        if let savedIsPlaying = sharedDefaults?.bool(forKey: "isPlaying") {
            isPlaying = savedIsPlaying
        }
    }

    private func setupCommandCenter() {
        let commandCenter = MPRemoteCommandCenter.shared()

        commandCenter.playCommand.addTarget { [unowned self] _ in
            self.playAudio()
            return .success
        }

        commandCenter.pauseCommand.addTarget { [unowned self] _ in
            self.pauseAudio()
            return .success
        }
    }

    func setupAudio(from url: String) {
        guard let url = URL(string: url) else {
            print("Invalid URL")
            return
        }

        let playerItem = AVPlayerItem(url: url)
        mainPlayer = AVPlayer(playerItem: playerItem)

        timeObserver = mainPlayer?.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1), queue: .main) { [weak self] time in
            let currentTime = CMTimeGetSeconds(time)
            self?.updateNowPlayingInfo()
            // 同步进度到共享存储
            self?.sharedDefaults?.set(currentTime, forKey: "currentProgress")
            self?.sharedDefaults?.synchronize()
        }

        playAudio()
    }

    func playAudio() {
        DispatchQueue.main.async {
            self.mainPlayer?.play()
            self.isPlaying = true
            self.updateNowPlayingInfo()
            // 同步状态到App Groups
            self.sharedDefaults?.set(true, forKey: "isPlaying")
            self.sharedDefaults?.synchronize()
            // 主动刷新Widget
            WidgetCenter.shared.reloadAllTimelines()
        }
    }

    func pauseAudio() {
        DispatchQueue.main.async {
            self.mainPlayer?.pause()
            self.isPlaying = false
            self.updateNowPlayingInfo()
            // 同步状态到App Groups
            self.sharedDefaults?.set(false, forKey: "isPlaying")
            self.sharedDefaults?.synchronize()
            // 主动刷新Widget
            WidgetCenter.shared.reloadAllTimelines()
        }
    }

    private func updateNowPlayingInfo() {
        let currentTime = CMTimeGetSeconds(mainPlayer?.currentTime() ?? CMTime.zero)
        let duration = CMTimeGetSeconds(mainPlayer?.currentItem?.duration ?? CMTime.zero)

        var nowPlayingInfo = [String: Any]()
        nowPlayingInfo[MPMediaItemPropertyTitle] = "Your Title"
        nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime
        nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
        nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = mainPlayer?.rate ?? 0.0
        MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
    }
}

2. Widget中读取共享状态

struct AudioPlayerWidgetEntry: TimelineEntry {
    let date: Date
    let isPlaying: Bool
}

struct AudioPlayerWidgetProvider: TimelineProvider {
    // 替换为你的App Group ID
    private let sharedDefaults = UserDefaults(suiteName: "com.your.app.group")

    func placeholder(in context: Context) -> AudioPlayerWidgetEntry {
        AudioPlayerWidgetEntry(date: Date(), isPlaying: false)
    }

    func getSnapshot(in context: Context, completion: @escaping (AudioPlayerWidgetEntry) -> Void) {
        let isPlaying = sharedDefaults?.bool(forKey: "isPlaying") ?? false
        completion(AudioPlayerWidgetEntry(date: Date(), isPlaying: isPlaying))
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<AudioPlayerWidgetEntry>) -> Void) {
        let isPlaying = sharedDefaults?.bool(forKey: "isPlaying") ?? false
        let entry = AudioPlayerWidgetEntry(date: Date(), isPlaying: isPlaying)
        // 设置1分钟刷新一次,可根据需求调整
        let timeline = Timeline(entries: [entry], policy: .after(Date().addingTimeInterval(60)))
        completion(timeline)
    }
}

struct AudioPlayerWidgetView: View {
    var entry: AudioPlayerWidgetProvider.Entry

    var body: some View {
        Button(action: {
            let intent = PlayBalanceTrackIntent()
            // 执行Intent
            let handler = PlayBalanceTrackIntentHandler()
            Task {
                _ = try? await handler.handle(intent)
            }
        }) {
            Image(systemName: entry.isPlaying ? "pause.circle.fill" : "play.circle.fill")
                .font(.largeTitle)
                .foregroundColor(.accentColor)
        }
    }
}

注意事项

  • Widget扩展不能直接创建和控制AVPlayer,所有播放逻辑必须由主应用处理,Widget只负责触发Intent和展示状态。
  • 确保主应用在后台时也能处理Intent的播放请求,需要配置音频后台模式(在Info.plist中添加UIBackgroundModes -> audio)。
  • MPNowPlayingInfoCenter的状态更新会自动同步到控制中心和锁屏界面,Widget也可以读取这些状态作为补充。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 13:10:54