如果你在开发一款相机应用,AVCaptureEventInteraction 几乎免费送你硬件按钮支持。但如果你在开发一款需要验证某个特定按钮是否正常工作的诊断应用,同一个 API 会变得出人意料地不配合。
这是我们在实现 Camera Control 测试时的经验总结——其中有几件事任何文档里都找不到,代价是一场手持真机的调试会话。
基础#
AVCaptureEventInteraction(iOS 17.2+)是一个附加到视图上的 UIInteraction。它提供两个处理器:
let interaction = AVCaptureEventInteraction(
primary: { event in
guard event.phase == .ended else { return }
capturePhoto()
},
secondary: { event in
guard event.phase == .ended else { return }
// ...
}
)
interaction.isEnabled = true
view.addInteraction(interaction)每个事件都携带一个 phase:按钮按下时是 .began,松开时是 .ended——触发拍摄就在这里——而当应用退到后台或拍摄不可用时是 .cancelled。
哪个按钮对应哪个处理器#
这是最容易出错的部分,而且后果是实打实的:
| 处理器 | 触发来源 |
|---|---|
primary | 音量减、Action button、Camera Control、AirPods 耳机柄按压(iOS 26) |
secondary | 音量加——除此之外没有别的 |
由此立刻得出两个结论。
Camera Control 永远不会触发 secondary。 如果你把某个功能挂在 secondary 处理器上,以为它是 Camera Control 手势,那么它实际上完全由音量加按钮驱动。我们就踩过这个坑:挂在 secondary 上的"切换摄像头"操作,只对音量加有反应。
省略 secondary 不等于忽略它。 如果你不提供 secondary 处理器,音量加的点击会落入 primary。想要吞掉音量加而不做任何响应,你仍然必须提供这个处理器——只是把它留空。
你无法区分这些按钮#
AVCaptureEvent 上没有任何标识来源的属性。我们对照 iOS 26 SDK 的机器生成 diff 核实过:这个类型只暴露 phase 和 shouldPlaySound,仅此而已。这种抽象是刻意为之——Apple 想传达的是"用户请求拍摄",而不是"用户按下了按键 X"。
对相机应用来说这没问题。但对必须证明某个特定按钮工作正常的测试来说,这是致命的:音量减的按压和 Camera Control 的点按在 API 层面无法区分。
唯一的例外是 AirPods。shouldPlaySound 仅在事件来自 AirPods 耳机柄按压、且你已通过 AVCaptureEventInteraction.defaultCaptureSoundDisabled 禁用默认拍摄提示音时才为 true。这足以把 AirPods 过滤出来,但对其余来源它什么也说明不了。
什么才能真正识别 Camera Control#
既然事件是匿名的,可靠的信号只能来自 controls API——它是 Camera Control 硬件独占的:
guard session.supportsControls else { return }
let zoom = AVCaptureSystemZoomSlider(device: device) { factor in
// 只有 Camera Control 能移动它——音量键做不到。
}
if session.canAddControl(zoom) { session.addControl(zoom) }
session.setControlsDelegate(self, queue: sessionQueue)之后有两个信号可用:
sessionControlsDidBecomeActive(_:)——控件浮层出现了,而只有轻按 Camera Control 才能触发它。- 滑块回调——沿 Camera Control 表面滑动。音量键无法移动滑块。
把两者结合起来就是一个可行的启发式方案:只在浮层处于活跃状态或刚关闭不久时接受 primary 事件。完整按压会在事件送达之前收起浮层,所以在 sessionControlsDidBecomeInactive 之后要留出几秒的时间窗口。
陷阱#
controls 委托需要一个活跃的 event interaction。 只添加控件并设置委托是不够的——视图层级中没有存活的 AVCaptureEventInteraction,委托就永远不会被调用,按钮也毫无反应。这是"AVCaptureSessionControlsDelegate 没有被调用"最常见的原因。
你的应用需要一个 Locked Camera Capture 扩展。 没有它,你的应用甚至不会出现在 Camera Control 的设置列表里。这个扩展不能是空壳——它必须带着自己的 capture interaction 真正使用相机,否则系统会终止它。
supportsControls 可能抛出 unrecognized selector——在没有 Camera Control 硬件的设备上。要加保护:
guard probe.responds(to: NSSelectorFromString("supportsControls")) else { return false }用户可以关掉你的检测。 设置 → Camera Control → Camera Adjustments 是一个面向用户的开关。一旦关闭,浮层不再出现,滑块也不再移动——你的两个 Camera Control 信号全部消失,只剩下匿名的 primary。没有公开 API 可以读取这个开关的状态,所以设计时要预留这种可能性。
滑块回调会连续不断地触发。 一次滑动会产生几十次调用,每个微小移动一次。任何"测试通过"或"拍摄"之类的操作都要用标志位保护,否则它会被反复执行。
不要在 deinit 里通过 [weak self] 操作会话。 在 deinit 内捕获 [weak self] 的闭包会对一个正在释放的对象形成弱引用,运行时会以 "Cannot form weak reference to instance … in the process of deallocation." 直接中止。改为直接捕获会话和队列:
deinit {
let session = session
let queue = sessionQueue
queue.async {
if session.isRunning { session.stopRunning() }
}
}iOS 26 改变了什么#
Capture controls 新增了对 AirPods 耳机柄按压的支持——使用 AVCaptureEventInteraction 的应用无需改动任何代码即可自动获得支持——此外还引入了 AVCaptureEventSound 和 playSound(_:),用于在系统快门音被禁用时手动处理快门声。
没有改变的是:仍然没有任何办法识别是哪个物理按钮产生了事件。
实用建议#
如果你在写相机应用,请忽略上面的一切,只处理 primary 就好。这层抽象正按设计意图工作。
如果你在写一个必须把事件归因到某个特定按钮的测试或诊断工具,请尽早接受这些限制:音量加可以完全识别,AirPods 在一个条件下可以识别,Camera Control 只有在其 adjustments 开启时才能识别。除此之外的一切都是抛硬币,再怎么在音频会话音量监听上耍聪明也绕不过去——capture interaction 在活跃期间会抑制音量变化,你根本没有东西可以观察。



