我正在为我的应用程序创建一个音频播放小部件,旨在实现与 Apple Music 小部件类似的功能。具体来说,我在小部件中实现了一个播放按钮,用于触发 AudioPlaybackIntent。然后,此意图与管理我的 AVAudioPlayer 的单例类进行交互。
我面临的问题是我的主应用程序中的 AVAudioPlayer 实例和我的小部件扩展中的实例似乎不共享相同的状态。
我注意到Apple Music小部件能够显示实时变化,这意味着它有某种方式在主应用程序和小部件之间共享播放状态。
如何在我的 AVAudioPlayer 实例的主应用程序和小部件之间实现共享状态? Apple Music 是否使用特定方法或 API 来实现这一目标?
这是我的代码流程:
struct PlayBalanceTrackIntent: AudioPlaybackIntent {
init() {
do {
try AVAudioSession.sharedInstance().setCategory(.playback)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("Failed to set up audio session category: \(error)")
}
}
init(trackURL: String) {
self.trackURL = trackURL
}
func perform() async throws -> some IntentResult {
if PlayerModel.shared.isPlaying {
PlayerModel.shared.pauseAudio()
} else {
PlayerModel.shared.setupAudio(from: trackURL)
}
return .result()
}
}
然后是简单的播放器
class PlayerModel: ObservableObject {
static let shared = PlayerModel()
@Published var mainPlayer: AVPlayer?
@Published var isPlaying: Bool = false
private var timeObserver: Any?
private init() {
setupCommandCenter()
}
deinit {
if let timeObserver = timeObserver {
mainPlayer?.removeTimeObserver(timeObserver)
}
}
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?.time = self?.formatTime(time: currentTime) ?? "00:00"
self?.updateNowPlayingInfo()
}
playAudio()
}
func playAudio() {
DispatchQueue.main.async {
self.mainPlayer?.play()
self.isPlaying = true
self.updateNowPlayingInfo()
}
}
func pauseAudio() {
DispatchQueue.main.async {
self.mainPlayer?.pause()
self.isPlaying = false
}
}
private func updateNowPlayingInfo() {
let currentTime = CMTimeGetSeconds(mainPlayer?.currentTime() ?? CMTime.zero)
time = formatTime(time: currentTime)
var nowPlayingInfo = [String: Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = "Your Title"
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = CMTimeGetSeconds(mainPlayer?.currentItem?.duration ?? CMTime.zero)
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = mainPlayer?.rate ?? 0.0
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
如果您使用
AudioPlaybackIntent
,那么意图将使用主应用程序进程。这意味着 perform
方法中的代码将访问与主应用程序相同的 PlayerModel
实例。
您需要的是将一些数据传递给 TimelineProvider 来更新视图 - 通常
UserDefaults
就足够了。
这是一个简单的例子:
class PlayerModel {
static let shared = PlayerModel()
func play() {
isPlaying = true
// ...
}
func pause() {
isPlaying = false
// ...
}
}
struct PlayIntent: AudioPlaybackIntent {
static var title: LocalizedStringResource = "Play Audio"
init() {}
func perform() async throws -> some IntentResult {
PlayerModel.shared.play()
UserDefaults.appGroup.set(
PlayerModel.shared.isPlaying,
forKey: "isAudioPlaying"
)
return .result()
}
}
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> Entry {
.placeholder
}
func getSnapshot(in context: Context, completion: @escaping (Entry) -> Void) {
completion(.placeholder)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
let isPlaying = UserDefaults.appGroup.bool(forKey: "isAudioPlaying")
let entry = Entry(isPlaying: isPlaying)
completion(.init(entries: [entry], policy: .never))
}
}
这里是一个 GitHub 存储库,其中包含不同的小部件示例,包括音频播放小部件。