Coping with non-Sendable KeyPath in a '@Sendable' closure

I am getting a warning in this code (the code is distilled from the actual version to remove noise):

import AVFoundation

actor MyModel {
    private let device: AVCaptureDevice
    init(device: AVCaptureDevice) { self.device = device }
    private var observation: NSKeyValueObservation?
    
    func doSomething<T>(_ keyPath: KeyPath<AVCaptureDevice, T>) {
        let newValue = device[keyPath: keyPath]
        print(newValue)
        // do something here
    }

    func makeObservation<T: Sendable & Equatable>(keyPath: KeyPath<AVCaptureDevice, T>) {
        // 🔶 workaround 1
        // let keyPath = unsafeBitCast(keyPath, to: (Sendable & KeyPath<AVCaptureDevice, T>).self) 
        self.observation = device.observe(keyPath) { _, _ in
            Task {
                await self.doSomething(keyPath)
                // ⚠️ Capture of 'keyPath' with non-Sendable type 'KeyPath<AVCaptureDevice, T>' in a '@Sendable' closure
            }
        }
    }
}
// 🔶 workaround 2
// extension KeyPath: @unchecked @retroactive Sendable {}

The warning is:

⚠️ Capture of 'keyPath' with non-Sendable type 'KeyPath<AVCaptureDevice, T>' in a '@Sendable' closure

Fair enough, KeyPath is not sendable. I have these three options in mind to deal with it:

  1. live with the warning (until – if ever – it becomes an error)
  2. use workaround 1 (unsafeBitCast key path to be sendable)
  3. use workaround 2 (retroactively conform KeyPath to unchecked sendable)

Neither of these look ideal, and 2 & 3 seem quite dangerous.
What is the best way coping with this warning?

nonisolated(unsafe) let keyPath = keyPath is also an option. It definitely still counts as a "workaround" in my book though.


The compiler is correct here, since arbitrary key paths may include non-sendable operations, so really you should push the Sendable requirement up to callers of makeObservation(keyPath:)…but then the nonisolated(unsafe) workaround stops working, and the other options are both still bad.

1 Like

You can constrain sendable key paths, which in this case should be fine since AVCaptureDevice is not actor-isolated:

 func makeObservation<T: Sendable & Equatable>(
-  keyPath: KeyPath<AVCaptureDevice, T>
+  keyPath: any KeyPath<AVCaptureDevice, T> & Sendable
 ) {
3 Likes