SwiftUI 将 .onReceive 通知与 AVPlayer 相结合

SwiftUI Combine for .onReceive notifications with AVPlayer

我在 SwiftUI 中工作,并且有一个 AudioPlayer 类型,它是 AVPlayer 的子class;它发布 AVPlayer 的 timeControllerStatus(?)(.playing、.paused 和其他?)。而不是 subclassing AVPlayer,我想传入一个 AVPlayer 并让它在某些视图中使用 .onReceive 通知我。这是我当前的功能类型:

import AVKit
import Combine

class AudioPlayer: AVPlayer, ObservableObject {
    @Published var buffering: Bool = false

    override init() {
        super.init()
        registerObservers()
    }

    private func registerObservers() {
        self.addObserver(self, forKeyPath: "timeControlStatus", options: [.old, .new], context: nil)
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

        if keyPath == "timeControlStatus", let change = change, let newValue = change[NSKeyValueChangeKey.newKey] as? Int, let oldValue = change[NSKeyValueChangeKey.oldKey] as? Int {
            let oldStatus = AVPlayer.TimeControlStatus(rawValue: oldValue)
            let newStatus = AVPlayer.TimeControlStatus(rawValue: newValue)
            if newStatus != oldStatus {
                DispatchQueue.main.async {[weak self] in
                    if newStatus == .playing || newStatus == .paused {
                        self?.buffering = false
                    } else {
                        self?.buffering = true
                    }
                }
            }
        }
    }
}

这里有一个 class 的例子,就像我想要的那样(取自 Chris Mash's tutorial on SwiftUI & AVPlayer):

import Combine
import AVFoundation

class PlayerItemObserver {
    let publisher = PassthroughSubject<Bool, Never>()
    private var itemObservation: NSKeyValueObservation?

    init(player: AVPlayer) {
        // Observe the current item changing
        itemObservation = player.observe(\.currentItem) { [weak self] player, change in
            guard let self = self else { return }
            // Publish whether the player has an item or not
            self.publisher.send(player.currentItem != nil)
        }
    }

    deinit {
        if let observer = itemObservation {
            observer.invalidate()
        }
    }
}

非常感谢您的帮助。

据我了解,您需要像文章示例中那样观察 timeControlStatus。为此,您只需替换观察者:

import Combine
import AVFoundation

class PlayerItemObserver {

    let controlStatusChanged = PassthroughSubject<AVPlayer.TimeControlStatus, Never>()
    private var itemObservation: NSKeyValueObservation?

    init(player: AVPlayer) {

        itemObservation = player.observe(\.timeControlStatus) { [weak self] player, change in
            guard let self = self else { return }
            self.controlStatusChanged.send(player.timeControlStatus)
        }

    }

    deinit {
        if let observer = itemObservation {
            observer.invalidate()
        }
    }
}

// MARK: init view
let player = AudioPlayer()
let playerObserver = PlayerItemObserver(player: player)
let contentView = SongListView(playerObserver: playerObserver)

// MARK: react on changing in view:
struct ContentView: View {

    let playerObserver: PlayerItemObserver

    var body: some View {
        Text("Any view")
            .onReceive(playerObserver.controlStatusChanged) { newStatus in
                switch newStatus {
                case .waitingToPlayAtSpecifiedRate:
                    print("waiting")
                case .paused:
                    print("paused")
                case .playing:
                    print("playing")
                }
        }
    }

}

UPDATE 您可以在没有 "old school" observe 的情况下使用 @PublishedAnyCancellable 实现相同的效果。最后一个甚至don't need extra code in deinit。这是这个解决方案:

import Combine
import AVFoundation

class PlayerItemObserver {

    @Published var currentStatus: AVPlayer.TimeControlStatus?
    private var itemObservation: AnyCancellable?

    init(player: AVPlayer) {

        itemObservation = player.publisher(for: \.timeControlStatus).sink { newStatus in
            self.currentStatus = newStatus
        }

    }

}

// MARK: you need to change view with new observation, but in general it will be the same
struct ContentView: View {

    let playerObserver: PlayerItemObserver

    var body: some View {
        Text("Any view")
            .onReceive(playerObserver.$currentStatus) { newStatus in
                switch newStatus {
                case nil:
                    print("nothing is here")
                case .waitingToPlayAtSpecifiedRate:
                    print("waiting")
                case .paused:
                    print("paused")
                case .playing:
                    print("playing")
                }
        }
    }

}

NSObject 有一种方法可以为任何 KVO-compliant 属性 提供 Publisher。它没有记录在案,但在 WWDC 2019 会议上进行了讨论。例如,Raleigh Ledet 对其进行了描述 starting at 25m36s in Session 231: Introducing SwiftUI, and Michael LeHew used it at 11m47s in Session 721: Combine in Practice.

方法是declared like this:

public func publisher<Value>(
    for keyPath: KeyPath<Self, Value>,
    options:NSKeyValueObservingOptions = [.initial, .new]
) -> NSObject.KeyValueObservingPublisher<Self, Value>

因此,例如,您可以这样使用它:

player.publisher(for: \.timeControlStatus, options: [.initial])
    .sink { print("player status: \([=11=])") }
    .store(in: &tickets)