Observe only changes made with KVO - swift

I have an AVPlayer object, and I'm observing the state changes using KVO; Changes to .timeControlStatus, which tells you whether the AVPlayer is playing, stopped, loading, or paused.
The problem is the AVPlayer sets the .timeControlStatus property to .waitingToPlayAtSpecifiedRate (means the file is loading) many, many times while the file is loading. I only want to be notified on the first change to loading, not subsequent settings. My observer is set up like this:
// watch .timeControlStatus
playerStatusToken = player?.observe(\.timeControlStatus, options: [.new], changeHandler: { [weak self] player, change in
if player.timeControlStatus == AVPlayer.TimeControlStatus.paused {
if self?.playerItemActive == false {
print("playback stopped")
self?.prevID = self!.currentID
self?.currentID = 0
self?.currentURL = nil
self?.playState = .stopped
}
else {
print("playback paused")
self?.playState = .paused
}
}
else if player.timeControlStatus == AVPlayer.TimeControlStatus.playing {
self?.playState = .playing
}
else if player.timeControlStatus == AVPlayer.TimeControlStatus.waitingToPlayAtSpecifiedRate {
self?.playState = .loading
}
})
Is there a way I can only respond to changes? I don't wan't to know if the .timeControlStatus is set to the same value as last time. Is there a way to do that (aside from keeping a variable called "lastStatus", and comparing to that).

Using Combine allows you to observe changes and filter out duplicate values out of the box
// retain var in your class
var cancellable: Cancellable?
// observe changes
cancellable = player?.publisher(for: \.timeControlStatus)
.removeDuplicates()
.sink() {
// your change handler
}

Related

How to set NowPlaying properties with a AVQueuePlayer in Swift?

I have an AVQueuePlayer that gets songs from a Firebase Storage via their URL and plays them in sequence.
static func playQueue() {
for song in songs {
guard let url = song.url else { return }
lofiSongs.append(AVPlayerItem(url: url))
}
if queuePlayer == nil {
queuePlayer = AVQueuePlayer(items: lofiSongs)
} else {
queuePlayer?.removeAllItems()
lofiSongs.forEach { queuePlayer?.insert($0, after: nil) }
}
queuePlayer?.seek(to: .zero) // In case we added items back in
queuePlayer?.play()
}
And this works great.
I can also make the lock screen controls appear and use the play pause button like this:
private static func setRemoteControlActions() {
let commandCenter = MPRemoteCommandCenter.shared()
// Add handler for Play Command
commandCenter.playCommand.addTarget { [self] event in
queuePlayer?.play()
return .success
}
// Add handler for Pause Command
commandCenter.pauseCommand.addTarget { [self] event in
if queuePlayer?.rate == 1.0 {
queuePlayer?.pause()
return .success
}
return .commandFailed
}
}
The problem comes with setting the metadata of the player (name, image, etc).
I know it can be done once by setting MPMediaItemPropertyTitle and MPMediaItemArtwork, but how would I change it when the next track loads?
I'm not sure if my approach works for AVQueueplayer, but for playing live streams with AVPlayer you can "listen" to metadata receiving.
extension ViewController: AVPlayerItemMetadataOutputPushDelegate {
func metadataOutput(_ output: AVPlayerItemMetadataOutput, didOutputTimedMetadataGroups groups: [AVTimedMetadataGroup], from track: AVPlayerItemTrack?) {
//look for metadata in groups
}
}
I added the AVPlayerItemMetadataOutputPushDelegate via an extension to my ViewController.
I also found this post.
I hope this gives you a lead to a solution. As said I'm not sure how this works with AVQueuePlayer.

Calling stopActivityUpdates is not stopping updates

After staring motionActivityUpdates, any attempt to stop it is not working. Here is the code that I am using for starting the motionactivity. There is a similar question without any answers and it is in C. Reading the documentation this is supposed to stop activity updates
Call this method to stop the delivery of updates that you started by calling the startActivityUpdates(to:withHandler:) method. This method does not stop queries started using the queryActivityStarting(from:to:to:withHandler:) method.
var manager: CMMotionActivityManager?
var motionManager: CMMotionManager?
func startMotionMonitoring() {
motionManager = CMMotionManager()
manager = CMMotionActivityManager()
startActivityMonitoring()
}
func startActivityMonitoring() {
if !CMMotionActivityManager.isActivityAvailable() {
return
}
if CMMotionActivityManager.authorizationStatus() != .authorized {
return
}
if let man = manager {
man.startActivityUpdates(to: .main) { (activity) in
guard let a = activity else {
return
}
print("motionActive")
}
}
}
I have another function I call to stop everything
func endMotionMonitoring(){
if manager == nil && motionManager == nil { return }
manager!.stopActivityUpdates()
manager = nil
motionManager!.stopAccelerometerUpdates()
motionManager = nil
}
But it is not stopping the motion activity updates. Every time I move the phone it prints out the statement "motionActive"
Anyone know how to fix this?

Observe a NSManagedObject in Swift 4 can cause crashes when modified in another thread?

What I have:
a NSManagedObject that sets a dynamic property to true when it's deleted from CoreData
override func prepareForDeletion() {
super.prepareForDeletion()
hasBeenDeleted = true
}
And within a view, I observe this NSManagedObject with the new Observe pattern of Swift 4
// I added this to observe the OBSERVED deletion to avoid a crash similar to:
// "User was deallocated while key value observers were still registered with it."
private var userDeletionObserver: NSKeyValueObservation?
private func observeUserDeletion() {
userDeletionObserver = user?.observe(\.hasBeenDeleted, changeHandler: { [weak self] (currentUser, _) in
if currentUser.hasBeenDeleted {
self?.removeUserObservers()
}
})
}
private func removeUserObservers() {
userDeletionObserver = nil
userObserver = nil
}
private var userObserver: NSKeyValueObservation?
private var user: CurrentUser? {
willSet {
// I remove all observers in willSet to also cover the case where we try to set user=nil, I think it's safer this way.
removeUserObservers()
}
didSet {
guard let user = user else { return }
// I start observing the NSManagedObject for Deletion
observeUserDeletion()
// I finally start observing the object property
userObserver = user.observe(\.settings, changeHandler: { [weak self] (currentUser, _) in
guard !currentUser.hasBeenDeleted else { return }
self?.updateUI()
})
}
}
So now, here come one observation and the question:
Observation: Even if I don't do the observeUserDeletion thing, the app seems to work and seems to be stable so maybe it's not necessary but as I had another crash related to the observe() pattern I try to be over careful.
Question details: Do I really need to care about the OBSERVED object becoming nil at any time while being observed or is the new Swift 4 observe pattern automatically removes the observers when the OBSERVED object is 'nilled'?

How to keep AVMIDIPlayer playing?

I'm trying to use Apple's AVMIDIPlayer object for playing a MIDI file. It seems easy enough in Swift, using the following code:
let midiFile:NSURL = NSURL(fileURLWithPath:"/path/to/midifile.mid")
var midiPlayer: AVMIDIPlayer?
do {
try midiPlayer = AVMIDIPlayer(contentsOf: midiFile as URL, soundBankURL: nil)
midiPlayer?.prepareToPlay()
} catch {
print("could not create MIDI player")
}
midiPlayer?.play {
print("finished playing")
}
And it plays for about 0.05 seconds. I presume I need to frame it in some kind of loop. I've tried a simple solution:
while stillGoing {
midiPlayer?.play {
let stillGoing = false
}
}
which works, but ramps up the CPU massively. Is there a better way?
Further to the first comment, I've tried making a class, and while it doesn't flag any errors, it doesn't work either.
class midiPlayer {
var player: AVMIDIPlayer?
func play(file: String) {
let myURL = URL(string: file)
do {
try self.player = AVMIDIPlayer.init(contentsOf: myURL!, soundBankURL: nil)
self.player?.prepareToPlay()
} catch {
print("could not create MIDI player")
}
self.player?.play()
}
func stop() {
self.player?.stop()
}
}
// main
let myPlayer = midiPlayer()
let midiFile = "/path/to/midifile.mid"
myPlayer.play(file: midiFile)
You were close with your loop. You just need to give the CPU time to go off and do other things instead of constantly checking to see if midiPlayer is finished yet. Add a call to usleep() in your loop. This one checks every tenth of a second:
let midiFile:NSURL = NSURL(fileURLWithPath:"/Users/steve/Desktop/Untitled.mid")
var midiPlayer: AVMIDIPlayer?
do {
try midiPlayer = AVMIDIPlayer(contentsOfURL: midiFile, soundBankURL: nil)
midiPlayer?.prepareToPlay()
} catch {
print("could not create MIDI player")
}
var stillGoing = true
while stillGoing {
midiPlayer?.play {
print("finished playing")
stillGoing = false
}
usleep(100000)
}
You need to ensure that the midiPlayer object exists until it's done playing. If the above code is just in a single function, midiPlayer will be destroyed when the function returns because there are no remaining references to it. Typically you would declare midiPlayer as a property of an object, like a subclassed controller.
Combining Brendan and Steve's answers, the key is sleep or usleep and sticking the play method outside the loop to avoid revving the CPU.
player?.play({return})
while player!.isPlaying {
sleep(1) // or usleep(10000)
}
The original stillGoing value works, but there is also an isPlaying method.
.play needs something between its brackets to avoid hanging forever after completion.
Many thanks.

Realm Results not auto-updating

I've got 2 models- Cards and Channels - and a Channel owns Cards
class Card : Object {
let channel = LinkingObjects(fromType: Channel.self, property: "cards")
}
class Channel: Object {
let cards = List<Card>()
}
In my UICollectionViewController, I'm using this as my Results:
var results: Results<Card> = {
let realm = try! Realm()
return realm.objects(Card).filter("ANY channel.is_following = true")
.filter("ANY channel.live = true")
.filter("published == true AND retracted == false").sorted("published_at", ascending: false)
}()
and my notification block set in my viewDidLoad:
notificationToken = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
print("in notif block")
}
I'm trying to make use of the new RealmCollectionChange features, but when I make a write/change to a Channel it isn't reflected in my Card results which should trigger the notification block. It shows the right stuff at first launch / initialization, but doesn't keep up with the changes after that. My pre-RealmCollectionChange code still works fine for me (catch all notifications and manually refresh my results), but wanted to see if anyone has any pointers before I roll back.
Thanks in advance.