WWDC Quick Look 💓 By SwiftGGTeam
Enhancing your camera experience with capture controls

Enhancing your camera experience with capture controls

观看原视频

Highlight

iOS 26 让 H2 芯片的 AirPods 通过按柄点按远程触发相机捕获,已接入 AVCaptureEventInteraction 的 app 自动获得此能力,无需修改一行代码。


核心内容

第三方相机 app 一直有个尴尬的体验断层:用户按系统相机习惯的「按音量键拍照」「长按录像」「用 Camera Control 调焦」,到了第三方 app 里全部失灵。屏幕上那颗虚拟快门按钮,远不如 iPhone 侧边的物理按键来得自然。更尴尬的是自拍场景——把手机架远后,没有任何办法触发拍摄。

WWDC25 这场 Session 由 AVKit 团队的 Vitaliy 和相机体验团队的 Nick 联合讲解,覆盖两套 API:AVCaptureEventInteraction 把音量键、Action 按钮、Camera Control 的物理按压事件转成 app 内的 capture 回调,6 行 SwiftUI 代码就能让第三方 app 用上系统相机般的物理按键体验;AVCaptureControl 则把 iPhone 16 的 Camera Control 暴露成可编程的硬件接口,支持连续滑块、离散滑块、Picker 三种控件。iOS 26 在此基础上加了一项新能力:H2 芯片的 AirPods 按柄点按可以远程触发主捕获事件,已经接入 AVCaptureEventInteraction 的 app 直接获得此功能,无需任何代码改动。


详细内容

AVCaptureEventInteraction:物理按键映射到捕获

物理按钮分为 primary 和 secondary 两类(03:37)。音量下键、Action 按钮、Camera Control 触发 primary action;音量上键触发 secondary action。同一个按钮不能同时触发两个 handler。secondary handler 是可选的——不指定时,音量上键也走 primary。

每次按压会发出一个事件通知,包含三个 phase(03:12):

  • began:按下瞬间,适合预热相机对象
  • cancelled:app 进入后台或捕获不可用时
  • ended:松开瞬间,这才是实际触发拍照的时机

SwiftUI 通过 onCameraCaptureEvent modifier 接入。一个最简的相机 view 加上物理按键支持,只要 6 行新代码(06:14):

import AVKit
import SwiftUI

struct PhotoCapture: View {
    let camera = CameraModel()
    var body: some View {
        VStack {
            CameraView()
            .onCameraCaptureEvent { event in
                if event.phase == .ended {
                   camera.capturePhoto()
                }
            }
            Button(action: camera.capturePhoto) {
                Text("Take a photo")
            }
        }
    }
}

关键点:

  • import AVKitonCameraCaptureEvent 由 AVKit 提供,不在 SwiftUI 默认 modifier 集合里
  • .onCameraCaptureEvent { event in ... } 挂在 CameraView 上,确保事件只在相机视图活跃时被消费
  • event.phase == .ended 才调用 capturePhoto(),避免在 began 阶段过早触发
  • 屏幕上的 Button 仍然保留——物理按键和虚拟按钮共用同一个 camera.capturePhoto 方法

注意:系统只把 capture event 发给「正在使用相机」的 app(04:22)。app 进后台或没有活跃的 AVCaptureSession,按键会回退到系统默认行为(调音量、启动相机)。一旦接入这个 API,必须响应所有收到的事件——不响应会让按钮变成哑键,体验很糟。

iOS 26:AirPods 按柄远程捕获

支持 H2 芯片的 AirPods 在 iOS 26 上获得远程相机捕获能力(06:38)。用户在「设置 → Remote Camera Capture」里选择「Press once」或「Press and hold」,然后在任何接入了 AVCaptureEventInteraction 的相机 app 中点按 AirPod 按柄即可触发主捕获。这个能力对开发者完全透明:已接入 API 的 app 自动获得。

因为用户拍照时可能没看屏幕,音频反馈变成关键。Apple 为此加了默认快门音,并提供了一个新的 API 让 app 自定义或禁用(09:13):

.onCameraCaptureEvent(defaultSoundDisabled: true) { event in
    if event.phase == .ended {
        if event.shouldPlaySound {
            event.play(.cameraShutter)
        }
    }
    camera.capturePhoto()
}

关键点:

  • defaultSoundDisabled: true 关掉系统默认提示音,避免和 app 自己的音效叠加
  • event.shouldPlaySound 仅在事件由 AirPod 按柄触发时为 true——音量键、Action 按钮、Camera Control 触发的事件都返回 false
  • event.play(.cameraShutter) 播放系统快门音;也可以播放 app bundle 里自带的音频文件
  • 即使关掉默认音,也务必在 AirPod 触发路径上播放某种反馈音——用户没看屏幕,全靠听

AVCaptureControl:把 Camera Control 编程化

Camera Control 是 iPhone 16 侧边的物理控件,支持点按拍照、轻按调出预览、滑动调节、双击切换设置(11:02)。AVFoundation 把它抽象成 AVCaptureControl,分两类:

  • Slider(数值):连续型可在范围内取任意值(如 zoom factor);离散型只取指定步进(如曝光补偿,±2 EV,1/3 步进)
  • Picker(列表):每个 index 对应一个命名状态(如 Flash 的 On/Off,Photographic Style 的名字)

系统预定义了两个子类:AVCaptureSystemZoomSliderAVCaptureSystemExposureBiasSlider,行为对齐系统相机。还有两个通用子类(AVCaptureSliderAVCaptureIndexPicker),用来定义自己的控件。

加一个系统变焦滑块到 session 上(14:46):

captureSession.beginConfiguration()

// configure device inputs and outputs

if captureSession.supportsControls {
    let zoomControl = AVCaptureSystemZoomSlider(device: device)

    if captureSession.canAddControl(zoomControl) {
        captureSession.addControl(zoomControl)
    }
}

captureSession.commitConfiguration()

关键点:

  • captureSession.supportsControls 先判断设备是否有 Camera Control(仅 iPhone 16 系列),不支持就跳过整段,避免在老设备上误用
  • AVCaptureSystemZoomSlider(device: device) 一行创建——系统控件直接拿到 device 引用,自动驱动 videoZoomFactor,不用 app 手动设置
  • canAddControl 是必要检查:例如 device 已经关联了一个 zoom control 时,再加第二个会失败
  • 整段包在 beginConfiguration / commitConfiguration 之间,按 AVCaptureSession 的标准事务模式提交

但只这样写会有同步问题:用户用 Camera Control 变焦后,UI 上的 pinch 手势模型还停留在旧值。修法是给 system control 传一个 action closure(15:40):

let zoomControl = AVCaptureSystemZoomSlider(device: device) { [weak self] zoomFactor in
    self?.updateUI(zoomFactor: zoomFactor)
}

关键点:

  • closure 在 main thread 上被回调(system control 的约定),可以直接更新 UI
  • [weak self] 防止控件持有 view controller 形成循环引用
  • 已经用 KVO 监听 videoZoomFactor 的项目可以省掉这个 closure,继续用原有机制

自定义控件:Reaction Effects Picker

App 自定义的 Picker,action callback 在你指定的 queue 上执行——典型做法是放到 session queue,因为它是管理 device 状态的隔离上下文(16:46):

let reactions = device.availableReactionTypes.sorted { $0.rawValue < $1.rawValue }
let titles = reactions.map { localizedTitle(reaction: $0) }
let picker = AVCaptureIndexPicker("Reactions", symbolName: "face.smiling.inverted",
    localizedIndexTitles: titles)

picker.isEnabled = device.canPerformReactionEffects
picker.setActionQueue(sessionQueue) { index in
    device.performEffect(for: reactions[index])
}

let controls: [AVCaptureControl] = [zoomControl, picker]

for control in controls {
    if captureSession.canAddControl(control) {
        captureSession.addControl(control)
    }
}

关键点:

  • availableReactionTypes 是无序集合,先 sorted 给 Picker 一个稳定顺序
  • AVCaptureIndexPicker 需要名称和 SF Symbol 图标——多控件场景下用户要靠图标识别这是哪个 Picker
  • picker.isEnabled = device.canPerformReactionEffects:控件不可用时置 disabled 而非省略,否则系统会 fallback 到另一个控件,用户会困惑
  • setActionQueue(sessionQueue):action 在 session queue 执行,因此 device.performEffect 调用与其它 device 配置串行,不会出现并发竞态
  • maxControlsCount 限制 session 上的控件总数,超过后 canAddControl 返回 false(16:46 提到)

核心启发

  • 做什么:给已有相机 app 接入 onCameraCaptureEvent,自动获得 iOS 26 AirPods 远程拍照

    • 为什么值得做:6 行代码换来一个完整的远程触发场景(自拍、合影、运动场景),是 iOS 26 上少见的「零成本新功能」
    • 怎么开始:在主拍摄 view 上挂 .onCameraCaptureEvent,把 event.phase == .ended 路由到现有的 capturePhoto() 方法,确保 AVCaptureSession 在前台时是 active 状态
  • 做什么:自定义快门音,对 AirPods 触发场景做差异化反馈

    • 为什么值得做:用户用 AirPods 拍照时不看屏幕,音效是唯一确认信号;默认提示音不一定贴合 app 的氛围(专业相机 vs. 滤镜 app 需要不同声音)
    • 怎么开始:用 defaultSoundDisabled: true 关掉默认音,在 closure 内判断 event.shouldPlaySound 决定是否播放——shouldPlaySound 只在 AirPod 路径下为 true,其它路径不会重复发声
  • 做什么:在 iPhone 16 上加一个 Photographic Style / 滤镜 Picker,挂到 Camera Control

    • 为什么值得做:用户已经习惯在 Camera Control 上「轻按两下切控件 → 滑动选择 → 轻按确认」的肌肉记忆,自定义 Picker 可以直接复用这套交互
    • 怎么开始:用 AVCaptureIndexPicker 包装你的滤镜列表,setActionQueue(sessionQueue) 把切换动作放到 session queue 上同步处理 device 配置;记得对不可用滤镜置 isEnabled = false 而非省略
  • 做什么:把 zoom / 曝光补偿换成系统预定义控件,去掉自实现

    • 为什么值得做:AVCaptureSystemZoomSlider 一行创建,行为完全对齐系统相机,包括步进、回弹、HUD 显示——自己写很难做到这种一致性
    • 怎么开始:删掉自定义的 zoom 控件实现,换成 AVCaptureSystemZoomSlider(device: device) { factor in updateUI(...) },UI 通过 closure 同步即可

关联 Session

评论

GitHub Issues · utterances