SwiftUI Combine для .onReceive уведомлений с AVPlayer - PullRequest
1 голос
/ 12 марта 2020

Я работаю в SwiftUI и имею тип AudioPlayer, который является подклассом AVPlayer; он публикует timeControllerStatus AVPlayer (?) (.playing, .paused и другие?). Вместо того, чтобы создавать подклассы AVPlayer, я хотел бы передать AVPlayer и уведомить меня, используя .onReceive в каком-то View. Вот текущий функциональный тип, который у меня есть:

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
                    }
                }
            }
        }
    }
}

А вот пример класса, подобного тому, который я хотел бы (взят из учебника Chris Ma sh по 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()
        }
    }
}

Ваша помощь очень ценится.

Ответы [ 2 ]

1 голос

Как я понимаю, нужно соблюдать 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")
                }
        }
    }

}

ОБНОВЛЕНИЕ вы можете добиться того же без "старой школы" observe, используя @Published и AnyCancellable. Последний даже не нуждается в дополнительном коде в 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")
                }
        }
    }

}
0 голосов
/ 12 марта 2020

NSObject имеет метод, который дает вам Publisher для любого KVO-совместимого свойства. Это не задокументировано, но обсуждалось на сессиях WWD C 2019. Например, Роли Ледет описал это начиная с 25m36s в Сессии 231: Представляя SwiftUI , и Майкл Лехью использовал его в 11m47s в Сессии 721: Объединение на практике .

Метод объявлен так :

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: \($0)") }
    .store(in: &tickets)
...