You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

锁屏LockedCameraCapture Widget点击无法启动APP,控制中心同款Widget可正常启动

锁屏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,但这个系统封装的控制器在锁屏扩展的受限沙箱里有很多隐性限制:

  1. 它的默认启动主应用逻辑在锁屏环境下被系统拦截,因为沙箱不允许它调用LaunchServices的完整能力
  2. 它的权限范围不足以触发主应用的映射,导致出现-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控制台的详细输出,排查是否有其他权限遗漏的提示~

火山引擎 最新活动