锁屏LockedCameraCapture Widget点击无法启动APP,控制中心同款Widget可正常启动
我刚碰到过几乎一模一样的问题,结合你贴的代码和报错信息,这个问题核心是锁屏环境的沙箱权限限制+UIImagePickerController的兼容性问题导致的,LaunchServices返回的-54错误就是典型的「受限环境下无法映射主应用数据库」的权限问题。下面给你一步步拆解分析和解决方法:
一、先搞懂错误码-54的本质
LaunchServices: store
or url was nil: Error Domain=NSOSStatusErrorDomain Code=-54 "process may not map database"
这个错误直白点说:锁屏环境下的Widget/扩展沙箱权限太严,LaunchServices没法正常定位并启动你的主应用。控制中心的Widget运行在相对宽松的沙箱里,所以UIImagePickerController的默认跳转逻辑能正常工作,但锁屏环境下这套逻辑直接失效了。
二、核心问题:UIImagePickerController不适合在LockedCameraCaptureExtension里用
你当前的Lock_Screen_CaptureViewFinder直接用了UIImagePickerController,但这个系统封装的控制器在锁屏扩展的受限沙箱里有很多隐性限制:
- 它的默认启动主应用逻辑在锁屏环境下被系统拦截,因为沙箱不允许它调用LaunchServices的完整能力
- 它的权限范围不足以触发主应用的映射,导致出现-54错误
苹果官方推荐在LockedCameraCaptureExtension里用AVCaptureSession实现相机捕获,这是专门针对扩展环境优化的底层API,兼容性和权限适配都更好。
三、具体修改步骤
1. 替换UIImagePickerController为AVCaptureSession实现
把你的Lock_Screen_CaptureViewFinder.swift完全替换成下面的代码,用AVCaptureSession实现相机预览和捕获,并且在捕获完成后显式启动主应用:
import SwiftUI import AVFoundation import LockedCameraCapture struct Lock_Screen_CaptureViewFinder: UIViewControllerRepresentable { let session: LockedCameraCaptureSession func makeUIViewController(context: Context) -> UIViewController { CameraPreviewViewController(session: session) } func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} } class CameraPreviewViewController: UIViewController, AVCapturePhotoCaptureDelegate { private let captureSession = AVCaptureSession() private let lockedSession: LockedCameraCaptureSession private var photoOutput = AVCapturePhotoOutput() init(session: LockedCameraCaptureSession) { self.lockedSession = session super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() setupCameraSession() setupPreviewLayer() // 绑定锁屏扩展的会话回调,触发相机捕获 lockedSession.onCapturePhoto = { [weak self] in self?.capturePhoto() } } private func setupCameraSession() { captureSession.sessionPreset = .photo // 配置后置相机输入 guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .rear) else { print("无法获取后置相机") return } do { let cameraInput = try AVCaptureDeviceInput(device: backCamera) if captureSession.canAddInput(cameraInput) { captureSession.addInput(cameraInput) } // 配置照片输出 if captureSession.canAddOutput(photoOutput) { captureSession.addOutput(photoOutput) } captureSession.startRunning() } catch { print("相机初始化失败:\(error.localizedDescription)") } } private func setupPreviewLayer() { let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) previewLayer.frame = view.bounds previewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(previewLayer) } private func capturePhoto() { let photoSettings = AVCapturePhotoSettings() if let previewFormat = photoSettings.availablePreviewPhotoPixelFormatTypes.first { photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: previewFormat] } photoOutput.capturePhoto(with: photoSettings, delegate: self) } // 捕获完成后,显式用URL Scheme启动主应用 func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { if let error = error { print("捕获照片失败:\(error)") lockedSession.finish() return } // 用你主应用注册的URL Scheme启动(需要先在主应用Info.plist里配置) guard let launchURL = URL(string: "myapp://camera-capture?photoData=\(photo.fileDataRepresentation()?.base64EncodedString() ?? "")") else { lockedSession.finish() return } // 扩展中启动主应用的正确方式 UIApplication.shared.open(launchURL, options: [:]) { success in if !success { print("启动主应用失败") } self.lockedSession.finish() } } }
2. 修正AppIntent的逻辑,正确启动锁屏相机会话
你的MyAppCaptureIntent需要明确启动LockedCameraCaptureSession,而不是依赖默认跳转:
import AppIntents import LockedCameraCapture struct MyAppContext: Codable {} struct MyAppCaptureIntent: CameraCaptureIntent { typealias AppContext = MyAppContext static let title: LocalizedStringResource = "启动相机捕获" static let description = IntentDescription("使用MyApp拍摄照片和视频") @MainActor func perform() async throws -> some IntentResult { // 启动锁屏相机会话 try await session.start() return .result() } }
3. 补全所有权限和Info.plist配置
- 主应用、LockedCameraCaptureExtension的Info.plist都要添加相机/麦克风权限描述:
<key>NSCameraUsageDescription</key> <string>需要相机权限来拍摄照片和视频</string> <key>NSMicrophoneUsageDescription</key> <string>需要麦克风权限来录制视频声音</string> - LockedCameraCaptureExtension的Info.plist要确保扩展配置正确:
<key>NSExtension</key> <dict> <key>NSExtensionAttributes</key> <dict> <key>LockedCameraCaptureExtensionSupportedMediaTypes</key> <array> <string>public.image</string> <string>public.movie</string> </array> </dict> <key>NSExtensionPrincipalClass</key> <string>$(PRODUCT_MODULE_NAME).Lock_Screen_Capture</string> </dict> - 主应用Info.plist里注册URL Scheme(比如
myapp://),这样扩展才能通过它启动主应用:<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLSchemes</key> <array> <string>myapp</string> </array> </dict> </array>
4. 验证Provisioning Profile配置
- 主应用、Widget扩展、LockedCameraCaptureExtension必须使用同一个Team ID的Provisioning Profile
- 确保Profile包含了「锁屏Widget」和「相机权限」的相关授权
四、为什么控制中心的Widget能正常工作?
控制中心的Widget运行在「半受限沙箱」里,系统允许它调用更多LaunchServices的能力,所以UIImagePickerController的默认跳转逻辑能生效;但锁屏Widget是在「完全受限沙箱」里运行,系统直接拦截了这种非显式的主应用启动请求,必须用UIApplication.shared.open(_:options:completionHandler:)这种显式方式才能触发主应用启动。
按照上面的步骤修改后,你的锁屏Widget应该就能正常启动主应用了,同时LaunchServices的-54错误也会消失。如果还有问题,可以看Xcode控制台的详细输出,排查是否有其他权限遗漏的提示~




