iPhone 16 拍照按钮适配

10/14/2024

拍照按钮功能简述

  1. 按钮的按下与抬起,在系统原生相机中的响应事件为开始录像或拍摄照片;
  2. 按钮的轻按和手势滑动:
    • 轻按分为单击和双击,单击时可以唤起侧边栏或选中功能菜单的功能,双击时侧边栏退回到上一层级;
    • 滑动时可以切换菜单中的功能或变更滑杆数值。

适配的条件和准备工作

  1. 以下所有适配在目前 iOS 18.0 SDK 下只能在摄像头启动的场景下有效,文档中特别提到 This API is for media capture use cases only.
  2. 按钮的按下与抬起需要通过 AVCaptureEventInteraction 监听,interaction 需要添加在一个与采集相关的 view 上,个人认为添加在管理拍摄的 viewController.view 上即可;
  3. 按钮的轻按和手势滑动需要根据功能设置对应的 Slider 或 Picker,这些组件都继承自 AVCaptureControl,初始化时需要指定使用的 AVCaptureDevice,并绑定在当前正在采集的 AVCaptureSession 上。
  4. 为了接收 AVCaptureControl 的回调事件,需要指定一个接收代理的对象和队列,并在对象内实现 AVCaptureSessionControlsDelegate 中的方法。

Demo 代码和说明

  1. 如前文第 4 条,在相机采集类中添加代理:
class CustomCameraCapture: NSObject {
    // 其他属性声明...
    lazy var captureSession: AVCaptureSession = AVCaptureSession()
    private lazy var interactionQueue: DispatchQueue = DispatchQueue(label: "MyInteractionQueue")
    
    // 其他方法...
    func startCapture() {
        // 处理其他开始录制的逻辑...
        if #available(iOS 18.0, *) {
            captureSession.setControlsDelegate(self, queue: interactionQueue)
        }
    }
    
}

// 实现 AVCaptureSessionControlsDelegate 中的方法,这些方法的目的是通知 AVCaptureControl 的生命周期
extension CustomCameraCapture: AVCaptureSessionControlsDelegate {
    
    func sessionControlsDidBecomeActive(_ session: AVCaptureSession) {
        debugPrint("[AVCaptureSessionControlsDelegate] sessionControlsDidBecomeActive")
    }
    
    func sessionControlsWillEnterFullscreenAppearance(_ session: AVCaptureSession) {
        debugPrint("[AVCaptureSessionControlsDelegate] sessionControlsWillEnterFullscreenAppearance")
    }
    
    func sessionControlsWillExitFullscreenAppearance(_ session: AVCaptureSession) {
        debugPrint("[AVCaptureSessionControlsDelegate] sessionControlsWillExitFullscreenAppearance")
    }
    
    func sessionControlsDidBecomeInactive(_ session: AVCaptureSession) {
        debugPrint("[AVCaptureSessionControlsDelegate] sessionControlsDidBecomeInactive")
    }
    
}
  1. 在相机采集类中提供一个给 viewController 设置 AVCaptureControl 的方法:
func handleInteractionsAndControls(on view: UIView) {
    // 虽然 AVCaptureEventInteraction 是 iOS 17.2 SDK 引入的,为了逻辑统一按照 iOS 18.0 做版本检查
    guard #available(iOS 18.0, *) else { return }
    guard let device = self.currentDevice else { return }
    self.clearInteractionsAndControls(on: view)

    // 设置 AVCaptureEventInteraction,根据 event.phase 确定按钮按下/抬起状态
    let interaction = AVCaptureEventInteraction { event in
        debugPrint("[AVCaptureEventInteraction] Event Phase: \(event.phase)")
    }
    view.addInteraction(interaction)

    // 设置系统原生的相机缩放值滑杆,回调 zoomFactor 为放大倍率,这个返回值可能有一些问题,将在后文中讨论
    let systemZoomSlider = AVCaptureSystemZoomSlider(device: device) { zoomFactor in
        debugPrint("[AVCaptureSystemZoomSlider] Zoom Factor: \(zoomFactor)")
    }

    // 设置系统原生的相机曝光值滑杆,回调 zoomFactor 为曝光度
    let systemBiasSlider = AVCaptureSystemExposureBiasSlider(device: device) { val in
        debugPrint("[AVCaptureSystemExposureBiasSlider] Value: \(val)")
    }

    // 设置自定义的滑杆,回调 val 为滑杆数值,初始化时的参数 symbolName 对应 SF Symbols 中的图标名称,in 后的参数为可调节的数值范围
    // 滑杆一般用于连续的数值选择,在原生相机中的使用场景是缩放、曝光、景深等
    let customSlider = AVCaptureSlider("Focus", symbolName: "scope", in: 0...2)
    customSlider.setActionQueue(self.interactionQueue) { val in
        debugPrint("[AVCaptureSlider] Value: \(val)")
    }

    // 设置自定义的选取框,回调 index 为选中的元素下标,初始化时的参数 symbolName 对应 SF Symbols 中的图标名称,localizedIndexTitles 为可选择的选项
    // 选取框一般用于离散的元素选择,在原生相机中的使用场景是选择滤镜或切换摄像头等
    let customPicker = AVCaptureIndexPicker("Filters", symbolName: "camera.filters", localizedIndexTitles: ["Filter 1", "Filter 2", "Filter 3", "Filter 4"])
    customPicker.setActionQueue(self.interactionQueue) { index in
        debugPrint("[AVCaptureIndexPicker] Index: \(index)")
    }

    // 开启 session 配置self.captureSession.beginConfiguration()
    let controls = [systemZoomSlider, systemBiasSlider, customSlider, customPicker]
    for control in controls {
        // 添加 control 前务必检查当前是否可以添加,直接添加可能存在个数溢出或报 NSInvalidArgumentException 异常
        if self.captureSession.canAddControl(control) {
            self.captureSession.addControl(control)
        }
    }
    // 提交 session 配置
    self.captureSession.commitConfiguration()
}

private func clearInteractionsAndControls(on view: UIView) {
    guard #available(iOS 18.0, *) else { return }
    view.interactions.forEach({ view.removeInteraction($0) })
    self.captureSession.controls.forEach({ self.captureSession.removeControl($0) })
}
  1. 在使用相机采集的 viewController 上调用方法:
self.customCamera?.handleInteractionsAndControls(on: self.view)

一些讨论

  1. AVCaptureControl 目前是黑盒的,我们只能按要求初始化并获取回调,无法自定义内部行为。特别说明这个问题是因为我个人体验下来,拍照按钮的滑动手感可能由于按钮的触控采样率相对屏幕较低,导致体感上并不非常跟手,但开发者没有办法优化内部的数据采集;
  2. AVCaptureSystemZoomSlider 回调的缩放值和滑杆上显示的数值不同,测试发现滑杆上显示的范围是1.0~5.0,但实际返回的数值是1.0~6.0,且两者之间符合线性关系 y=1.25x-0.25。这个问题在苹果开发者论坛上也有帖子讨论但目前没有官方回复。

参考文档

Enhancing your app experience with the Camera Control

Human Interface Guidelines / Camera Control

AVCaptureEventInteraction

AVCaptureControl