3

I'm trying to pause my AVPlayer when the item finishes playing. What is the best way to do this with SwiftUI? I don't know much about notifications, where to declare them, etc. Is there a way to use Combine for this? Sample code would be awesome! Thank you in advance.

UPDATE:

With help from the answer below, I managed to make a class which takes an AVPlayer and publishes a notification when the item ends. You can subscribe to the notification with the following:

Class:

import Combine
import AVFoundation

class PlayerFinishedObserver {

    let publisher = PassthroughSubject<Void, Never>()

    init(player: AVPlayer) {
        let item = player.currentItem

        var cancellable: AnyCancellable?
        cancellable = NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime, object: item).sink { [weak self] change in
            self?.publisher.send()
            print("Gotcha")
            cancellable?.cancel()
        }
    }
}

Add to your struct:

let finishedObserver: PlayerFinishedObserver

Subscribe on some View:

.onReceive(finishedObserver.publisher) {
                print("Gotcha!")
            }
atdonsm
  • 265
  • 4
  • 11

1 Answers1

5

I found one solution for similar issue:

  1. I created the new subclass of AVPlayer;
  2. Added observer to currentItem;
  3. Override func observeValue, where add observer for current item when player reach end time;

Here is simplified example:

import AVKit // for player
import Combine // for observing and adding as environmentObject

final class AudioPlayer: AVPlayer, ObservableObject {

    var songDidEnd = PassthroughSubject<Void, Never>() // you can use it in some View with .onReceive function

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

    private func registerObserves() {
        self.addObserver(self, forKeyPath: "currentItem", options: [.new], context: nil)
        // example of using 
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        // currentItem could be nil in the player. I add observer to exist item
        if keyPath == "currentItem", let item = currentItem {
            NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(_:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item)

            // another way, using Combine
            var cancellable: AnyCancellable?
            cancellable = NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime, object: item).sink { [weak self] _ in
                self?.songDidEnd.send()
                cancellable?.cancel()
            }
        }
        // other observers
    }

    @objc private func playerDidFinishPlaying(_ notification: Notification) {
        playNextSong() // my implementation, here you can just write: "self.pause()"
    }

}

UPDATE: simple example of using .onReceive (be careful, I wrote it without playground/Xcode, so it can have errors):

struct ContentView: View {

    @EnvironmentObject var audioPlayer: AudioPlayer
    @State private var someText: String = "song is playing"

    var body: some View {
        Text(someText)
            .onReceive(self.audioPlayer.songDidEnd) { // maybe you need "_ in" here
                self.handleSongDidEnd()
        }
    }

    private func handleSongDidEnd() {
        print("song did end")
        withAnimation {
            someText = "song paused"
        }
    }

}

About Combine with AVPlayer: you can look at my question, there you'll see some ways to observe playing time and functionality to rewind time with slider in SwiftUI.

I'm using one instance of AudioPlayer, controlling play/pause functions or changing currentItem (which means setting another song) like this:

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    // other staff
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        let homeView = ContentView()
            .environmentObject(AudioPlayer())

        // other staff of SceneDelegate
    }
}
Hrabovskyi Oleksandr
  • 3,070
  • 2
  • 17
  • 36
  • Thanks so much for this! I will try it out soon. Do you know if there is a way to use something more like this? NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime) and then use .(onReceive: ) to respond? I’m wondering if there is a way to avoid @objc. A lot of my AVPlayer functions are in my SwiftUI view, and it won’t let me use objc there because it’s a struct. Or should I move the functions into the AudioPlayer is class? – atdonsm Mar 02 '20 at 11:27
  • @StormTrooper you can change some ```Publisher``` inside ```@objc``` function, like ```songDidEnd.send()``` (you need to create ```let songDidEnd = PassthroughSubject()``` for example). so you can use than use ```.onReceive``` as usual. About ```NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime)``` - interesting, maybe I'll try to use it. But no, I don't know this way, so I can't say, will it work or not at this moment – Hrabovskyi Oleksandr Mar 02 '20 at 11:41
  • OK, thanks. I’m going to mess around with it and get back to you. I really appreciate your help. – atdonsm Mar 02 '20 at 11:42
  • @StormTrooper , your idea was right, you can write this: ```NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime, object: item)``` instead my variant. thanks for this idea) – Hrabovskyi Oleksandr Mar 02 '20 at 11:47
  • @StormTrooper I updated code snippet, there is example with your idea. I didn't test it, but you can try. It seems it will work – Hrabovskyi Oleksandr Mar 02 '20 at 11:56
  • I'm sorry but I'm really new to this. What would I put in the .onReceive(: ) area? – atdonsm Mar 02 '20 at 15:04
  • @StormTrooper you can put into everything you want. Look at my question (https://stackoverflow.com/questions/58779184/how-to-control-avplayer-in-swiftui), there you'll find example (maybe not the best one). when the view detects data emitted by publisher, you can perform some special action. I'll try to add some example into answer it three minutes. – Hrabovskyi Oleksandr Mar 02 '20 at 15:42
  • I got it to work with this: .onReceive(self.player.songDidEnd) { _ in print("Item ended") }. Thank you so much for your help. I may reach out again if I have more questions if that's okay. – atdonsm Mar 02 '20 at 19:41
  • Hi Александр, can you think of a way to get the onReceive notification for the song ending without subclassing AVPlayer? For example, could you make a class to which you pass in the current AVPlayer and have it notify when the item finishes? – atdonsm Mar 11 '20 at 16:13
  • Actually, I think I figured it out. You can see my update to the question if you're interested. – atdonsm Mar 11 '20 at 17:05