打造优秀的锁屏相机拍摄体验
Build a great Lock Screen camera capture experience
2024年6月10日
一句话判断
如果你的 App 有拍照功能,iOS 18 的 LockedCameraCapture 框架让你能在锁屏状态下直接提供拍摄体验——但隐私限制和安全约束决定了这不是一个简单的扩展点。
这场 Session 讲了什么
iOS 18 引入了 LockedCameraCapture 框架,允许第三方 App 在锁屏状态下提供相机拍摄体验。用户可以从控制中心、Action Button 或锁屏控件直接启动你的拍摄界面,无需解锁设备。这是对系统相机 App 锁屏能力的开放。
Session 详细介绍了 Locked Camera Capture Extension 的完整生命周期:从锁屏启动 -> 拍摄内容 -> (可选)请求解锁转场到主 App -> 扩展被 dismissed -> 内容传递给主 App 处理。这个生命周期中每个阶段都有对应的系统行为和开发者需要处理的边界情况。
安全是贯穿全场的核心主题。因为扩展运行在锁屏状态下,系统施加了严格的限制:必须立即展示相机取景器(否则会被系统终止)、必须使用 AVCaptureEventInteraction 处理硬件按钮事件、不能访问网络、不能读写共享容器、不能访问 App 的共享偏好设置。PhotoKit 是保存拍摄内容的推荐方式,它在锁屏状态下也有专门的安全模型。
值得深挖的点
锁屏状态下的数据隔离机制
LockedCameraCapture 的数据传递设计值得仔细研究。扩展在运行时有一个专用的文件目录,拍摄的内容可以保存在这里。当扩展被 dismissed(无论是转场到主 App 还是用户滑回锁屏),系统会将这个目录的内容移动到主 App 可访问的位置。主 App 在下次获得运行时,可以读取这些内容并做进一步处理。
对于使用 PhotoKit 保存照片的情况,安全模型更加细粒度。在锁屏状态下,PhotoKit 只允许读取当前拍摄会话中写入的照片——用户之前相册中的照片对扩展完全不可见。只有设备解锁后,扩展才能读取用户授权范围内的所有照片。这个设计确保了锁屏拍摄不会成为访问用户隐私照片的跳板。
硬件按钮事件的强制要求
Session 明确要求扩展必须使用 AVCaptureEventInteraction 来处理硬件按钮事件(音量键拍照/录像)。这不是建议而是硬性要求——不使用这个 API 的扩展可能无法通过审核。
AVCaptureEventInteraction 封装了硬件按钮的按压事件,支持单击拍照和长按录像。这个 API 的好处是提供了和系统相机一致的操作体验,用户不需要学习新的交互方式。但它的限制也很明显:你不能自定义按钮映射,不能添加新的硬件触发方式。设计拍摄界面时需要围绕这个约束来做。
代码片段
场景一:创建 Locked Camera Capture Extension 的基本结构
import LockedCameraCapture
import SwiftUI
// 扩展的入口点
class CaptureExtension: LockedCameraCaptureExtension {
override func makeConfiguration(
action: LockedCameraCaptureAction
) -> some LockedCameraCaptureScene {
LockedCameraCaptureScene { context in
// 必须立即展示相机取景器
// 如果不展示,系统会终止扩展
CameraView(captureContext: context)
}
}
}
// 在 Xcode 中添加新 target:
// File -> New -> Target -> Locked Camera Capture Extension
// 这会自动配置 Info.plist 和 entitlements
// 坑点:扩展是独立的 target,不会自动共享主 App 的代码
// 需要通过 framework 或 shared source 来复用代码
场景二:使用 PhotoKit 在锁屏状态下保存照片
import Photos
import AVFoundation
class CameraViewModel: ObservableObject {
let captureSession = AVCaptureSession()
let context: LockedCameraCaptureExtensionContext
init(context: LockedCameraCaptureExtensionContext) {
self.context = context
setupCamera()
}
func capturePhoto() {
let settings = AVCapturePhotoSettings()
photoOutput.capturePhoto(
with: settings,
delegate: PhotoDelegate(context: context)
)
}
}
class PhotoDelegate: NSObject, AVCapturePhotoCaptureDelegate {
let context: LockedCameraCaptureExtensionContext
func photoOutput(
_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?
) {
guard let data = photo.fileDataRepresentation() else { return }
// 使用 PhotoKit 保存到相册
// 在锁屏状态下,只能保存,不能读取之前的照片
PHPhotoLibrary.shared().performChanges {
let request = PHAssetCreationRequest.forAsset()
request.addResource(with: .photo, data: data, options: nil)
}
// 坑点:扩展继承主 App 的 Photos 权限
// 如果主 App 只有"添加照片"权限,扩展也只能添加
// 如果需要更多权限,必须转场到主 App 去请求
}
}
场景三:从扩展转场到主 App 并传递上下文
// 在扩展中请求转场到主 App
struct CameraView: View {
let context: LockedCameraCaptureExtensionContext
var body: some View {
CameraPreviewView()
.overlay {
VStack {
Spacer()
HStack {
Button("查看更多") {
// 请求打开主 App
// 系统会要求用户解锁设备
context.openApplication(
whenApproved: [:] // 可以传递自定义数据
)
}
// 必须使用 AVCaptureEventInteraction 处理硬件按钮
// 这是硬性要求
}
}
}
}
}
// 在主 App 中接收扩展传来的内容
// 当 App 获得运行时,检查扩展保存的内容目录
func processCapturedContent() {
// 系统已将扩展目录的内容移动到 App 可访问的位置
// 读取并处理拍摄的照片/视频
// 比如应用滤镜、上传到云端等
}
// 坑点:转场到 App 会触发设备解锁
// 如果用户取消解锁,转场不会发生,扩展继续运行
// 需要处理用户取消解锁的情况
最佳实践
- 迁移建议:如果你的 App 已有拍照功能,添加 Locked Camera Capture Extension 的工作量中等。核心工作是把拍摄界面抽取为可复用的 SwiftUI 视图,然后在扩展 target 中引用。建议先实现最基本的拍照+保存功能,再逐步添加高级特性。
- 隐私优先设计:扩展中不要尝试访问任何超出拍摄所需的数据。网络不可用、共享容器不可读,这些限制不是 bug 而是 feature。设计扩展架构时就按”完全离线、完全隔离”的假设来做。
- 用户体验一致性:拍摄界面应该和主 App 中的拍摄体验保持一致。用户不应该感知到”这是扩展”和”这是主 App”的区别。AVCaptureEventInteraction 确保了硬件操作的一致性,UI 层面也需要保持一致。
还有什么值得关注
- 扩展可以通过 Action Button 启动,为户外运动、水下拍摄等场景提供了快速入口
- PhotoKit 的 write-only 权限模式专门为锁屏场景设计,即使 App 有完整相册权限,锁屏状态下也只能写入
- Session 提到内容从扩展传递到主 App 的机制是系统级文件移动,不需要开发者手动处理文件传输