16

I just updated to Swift 4 and Xcode 9 and got a (swiftlint) warning for the following code telling me that I should use KVO now:

Warning:

(Block Based KVO Violation: Prefer the new block based KVO API with keypaths when using Swift 3.2 or later. (block_based_kvo))

The old code:

override func observeValue(forKeyPath keyPath: String?,
                           of object: Any?,
                           change: [NSKeyValueChangeKey : Any]?,
                           context: UnsafeMutableRawPointer?) {
    if keyPath == "outputVolume"{
        guard let newKey = change?[NSKeyValueChangeKey.newKey] as? NSNumber else {
            fatalError("Could not unwrap optional content of new key")
        }

        let volume = newKey.floatValue

        print("volume " + volume.description)
    }
}

My attempt to fix:

let audioSession = AVAudioSession.sharedInstance()
audioSession.observe(\.outputVolume) { (av, change) in
        print("volume \(av.outputVolume)")
}

Apple claims here that most of the properties should be dynamic (I know that this is AVPlayer and not AVAudioSession). I looked it up but couldn't find any dynamic statements inside AVPlayer properties and was wondering how that could possibly work (If I'm not mistaken those are required for KVO to work).

EDIT:

I'm not certain if it doesn't trigger because it simply doesn't work or if it's due to what I try to archive. In general I'll want to get notified on volume changes triggered by pushing the hardware-volume-rockers.

Caleb
  • 124,013
  • 19
  • 183
  • 272
Eternal Black
  • 259
  • 2
  • 15
  • Your attempt seems to work but don't forget to use or not use return value of observe function. If you don't want to use, you can do like this `_ = audioSession.observe(\.outputVolume) { (av, change) in print("volume \(av.outputVolume)") }` – abdullahselek Sep 20 '17 at 14:54
  • 2
    I tried that but unfortunately it doesn't seem to trigger. The code doesn't get executed for some reason. Maybe I'm missing something. – Eternal Black Sep 21 '17 at 08:27
  • Does this work in the simulator? Because I've tried everything and the observe callback is never called. – hornobster Jan 21 '18 at 11:38
  • No, this wont work in siuulator. Works for me on a device. – Jugoslav M. Jul 28 '20 at 11:21
  • 1
    I finally got this working (iOS 14/SDK 12). As others point out, you need to call `AVAudioSession.sharedInstance().setActive(true)` before you `observe()`. In my case, though, that alone didn't fix it. I had to put some separation _between_ the two calls. Set the session active in `init()`, then `observe()` in a later method. I'm sure a `dispatch_after` would solve it, too. – Reid Feb 09 '21 at 19:56

2 Answers2

19

I assume you're referring to the line:

You can use Key-value observing (KVO) to observe state changes to many of the player’s dynamic properties...

This use of "dynamic" isn't the same thing as Objective-C's @dynamic or Swift's dynamic. The docs just mean "properties that change" in this context, and they're telling you that the AVPlayer is generally very KVO-compliant and intended to be observed that way. "KVO compliant" means it follows the change notification rules. There are many ways to achieve that, both automatic and manual. The docs are just promising that AVPlayer does.

(An important point about Cocoa that distinguishes it from many other systems is that Cocoa handles many things "by convention". There's no way to say in code "this is KVO compliant" and there is no way for the compiler to enforce it, but Cocoa developers tend to be very good about following the rules. When ARC was developed, it relied heavily on the fact that Cocoa developers had for years named methods following very specific rules that indicate how memory management is handled. It just added complier enforcement of the rules Cocoa developers had always followed by hand. This is why Cocoa developers get very noisy about naming conventions and capitalization. There are major parts of Cocoa that rely entirely on following consistent naming rules.)

Remembering that the AVPlayer interface is an Objective-C API that happens to be bridged to Swift, there's no equivalent of the Swift keyword dynamic in that case. That's a keyword that tells Swift that this property may be observed and so its accessors can't be optimized to static dispatch. That's not something Objective-C requires (or can do; all ObjC properties are "dynamic" in this sense).

The Objective-C @dynamic is a completely different thing, only weakly related to KVO (though it comes up in a lot of KVO-heavy contexts like Core Data). It just means "even though you can't find an accessor implementation for this property anywhere, trust me, by the time this runs an implementation will be available." This relies on the ability of ObjC's runtime to generate implementations dynamically or dispatch in programmer-controlled ways (this still kind of exists in Swift by manipulating the ObjC runtime, but it isn't really a "Swift" feature).

As for how KVO works, it's one of the few true "magic tricks" in Cocoa. For a quick intro, see Key-Value Observing Implementation Details. The short version is:

  • When you observe an object, a subclass for that object is dynamically created (yes, a new class is invented at runtime).
  • The subclass adds calls to willChangeValue... and didChangeValue... around all calls to the superclass's property accessors.
  • The object is "ISA-swizzled" to be that new class.
  • Magic! (Ok, not really magic; it's just code, but it's quite a trick.)

EDIT: The original question never mentioned that it wasn't working. The reason it's not working is because you're not assigning the returned NSKeyValueObservation in a property; you're just throwing it away. I'm surprised there's not a warning about that; I may open a radar.

When the returned NSKeyValueObservation deallocates, the observation goes away, so this creates an observation and immediately destroys it. You need to store it in a property until you want the observation to go away.

mfaani
  • 33,269
  • 19
  • 164
  • 293
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 5
    so how does he fix the problem? – João Nunes Sep 22 '17 at 12:09
  • 1
    THANK YOU ROB! I've been a whole day struggling with this. I had everything like it was supposed to be but it didn't worked until I read your EDIT. Seems obvious once you know but there isn't any mention to this in the few words about new Swift 4 KVO that are available right now in the internet. +1 for the detailed explanation – rmvz3 Sep 24 '17 at 13:01
  • Thank you very much for the in depth answer. You are totally right and this makes sense if one knows the logic behind. I unfortunately at first stored the result in a variable and not in a class property.... – Eternal Black Sep 29 '17 at 14:18
8

Solution by OP.

It needs to be stored in a property. Not a variable, not _ but a property. Otherwise it won't work. Like this:

class YourViewController: UIViewController {

    var obs: NSKeyValueObservation?

    override func viewDidLoad() {
        super.viewDidLoad()

        let audioSession = AVAudioSession.sharedInstance()
        self.obs = audioSession.observe( \.outputVolume ) { (av, change) in
            print("volume \(av.outputVolume)")
        }
    }
} 
Cœur
  • 37,241
  • 25
  • 195
  • 267
  • Does this work in the simulator as well or just on the device? – Adam Freeman Apr 04 '19 at 02:02
  • @AdamFreeman KVO in general works on simulator. Regarding specifically [`AVAudioSession.outputVolume`](https://developer.apple.com/documentation/avfoundation/avaudiosession/1616533-outputvolume) on simulator, I've never tried it: the answer was written by Eternal Black. – Cœur Apr 04 '19 at 02:37
  • (and if it doesn't work on simulator, feel free to report it to Apple) – Cœur Apr 04 '19 at 02:42