├── .gitignore ├── Doc ├── hero.jpg ├── anatomy.jpg ├── debugger.png └── custom_flags.png ├── LockedWidget ├── Assets.xcassets │ ├── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── WidgetBackground.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist └── LockedWidgetBundle.swift ├── LockedCameraCaptureExtensionDemo ├── Assets.xcassets │ ├── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ └── AppIcon.appiconset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── LockedCameraCaptureExtensionDemo.entitlements ├── DemoApp.swift ├── Camera │ ├── AppStorageConfigProvider.swift │ ├── AppUserDefaultSettings.swift │ ├── OpenMainAppAction.swift │ ├── ViewModel │ │ ├── CamPreviewViewModel.swift │ │ ├── CaptureProcessor.swift │ │ └── MainViewModel.swift │ └── View │ │ ├── CaptureInteractionView.swift │ │ └── ContentView.swift ├── Intent │ └── AppCaptureIntent.swift └── Localizable.xcstrings ├── Packages ├── Models │ ├── .gitignore │ ├── Tests │ │ └── ModelsTests │ │ │ └── ModelsTests.swift │ ├── Sources │ │ └── Models │ │ │ └── CameraPosition.swift │ └── Package.swift └── MetalLib │ ├── .gitignore │ ├── Tests │ └── MetalLibTests │ │ └── MetalLibTests.swift │ ├── Package.swift │ └── Sources │ └── MetalLib │ ├── ViewRepresentable.swift │ ├── MetalView.swift │ └── MetalRenderer.swift ├── LockedCameraCaptureExtensionDemo.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcuserdata │ └── juniperphoton.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── xcshareddata │ └── xcschemes │ │ ├── LockedCameraCaptureExtensionDemo.xcscheme │ │ ├── LockedExtension.xcscheme │ │ └── LockedWidgetExtension.xcscheme └── project.pbxproj ├── LockedExtension ├── Info.plist ├── LockedExtension.swift └── OpenMainAppButton.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ltex.dictionary.en-US.txt 2 | -------------------------------------------------------------------------------- /Doc/hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuniperPhoton/LockedCameraCaptureExtensionDemo/HEAD/Doc/hero.jpg -------------------------------------------------------------------------------- /Doc/anatomy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuniperPhoton/LockedCameraCaptureExtensionDemo/HEAD/Doc/anatomy.jpg -------------------------------------------------------------------------------- /Doc/debugger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuniperPhoton/LockedCameraCaptureExtensionDemo/HEAD/Doc/debugger.png -------------------------------------------------------------------------------- /Doc/custom_flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JuniperPhoton/LockedCameraCaptureExtensionDemo/HEAD/Doc/custom_flags.png -------------------------------------------------------------------------------- /LockedWidget/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Packages/Models/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /Packages/MetalLib/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /LockedWidget/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LockedWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Packages/Models/Tests/ModelsTests/ModelsTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import Models 3 | 4 | @Test func example() async throws { 5 | // Write your test here and use APIs like `#expect(...)` to check expected conditions. 6 | } 7 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Packages/MetalLib/Tests/MetalLibTests/MetalLibTests.swift: -------------------------------------------------------------------------------- 1 | import Testing 2 | @testable import MetalLib 3 | 4 | @Test func example() async throws { 5 | // Write your test here and use APIs like `#expect(...)` to check expected conditions. 6 | } 7 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo.xcodeproj/xcuserdata/juniperphoton.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /LockedWidget/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.widgetkit-extension 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/LockedCameraCaptureExtensionDemo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/DemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoApp.swift 3 | // LockedCameraCaptureExtensionDemo 4 | // 5 | // Created by Photon Juniper on 2024/8/20. 6 | // 7 | 8 | import SwiftUI 9 | import AppIntents 10 | 11 | @main 12 | struct LockedCameraCaptureExtensionDemoApp: App { 13 | var body: some Scene { 14 | WindowGroup { 15 | ContentView(configProvider: AppStorageConfigProvider.standard) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LockedExtension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UISupportedInterfaceOrientations 6 | 7 | UIInterfaceOrientationPortrait 8 | 9 | EXAppExtensionAttributes 10 | 11 | EXExtensionPointIdentifier 12 | com.apple.securecapture 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Packages/Models/Sources/Models/CameraPosition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraPosition.swift 3 | // Models 4 | // 5 | // Created by JuniperPhoton on 2024/11/12. 6 | // 7 | import AVFoundation 8 | 9 | public enum CameraPosition: String, Codable, Sendable { 10 | case front 11 | case back 12 | 13 | public var avFoundationPosition: AVCaptureDevice.Position { 14 | switch self { 15 | case .front: 16 | return .front 17 | case .back: 18 | return .back 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LockedWidget/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | } 30 | ], 31 | "info" : { 32 | "author" : "xcode", 33 | "version" : 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/Camera/AppStorageConfigProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppStorageConfigProvider.swift 3 | // LockedCameraCaptureExtensionDemo 4 | // 5 | // Created by Photon Juniper on 2024/8/21. 6 | // 7 | import Foundation 8 | import LockedCameraCapture 9 | 10 | struct AppStorageConfigProvider : Sendable { 11 | let rootURL: URL? 12 | } 13 | 14 | extension AppStorageConfigProvider { 15 | static let standard = AppStorageConfigProvider( 16 | rootURL: getStandardRootURL() 17 | ) 18 | } 19 | 20 | @available(iOS 18, *) 21 | extension AppStorageConfigProvider { 22 | init(_ session: LockedCameraCaptureSession) { 23 | self.rootURL = session.sessionContentURL 24 | } 25 | } 26 | 27 | private func getStandardRootURL() -> URL? { 28 | try? FileManager.default.url( 29 | for: .documentDirectory, 30 | in: .userDomainMask, 31 | appropriateFor: nil, 32 | create: true 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /LockedWidget/LockedWidgetBundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LockedWidgetBundle.swift 3 | // LockedWidget 4 | // 5 | // Created by Photon Juniper on 2024/8/20. 6 | // 7 | 8 | import WidgetKit 9 | import SwiftUI 10 | 11 | @main 12 | struct LockedWidgetBundle: WidgetBundle { 13 | var body: some Widget { 14 | if #available(iOS 18, *) { 15 | LockedWidgetControl() 16 | } 17 | } 18 | } 19 | 20 | @available(iOS 18, *) 21 | struct LockedWidgetControl: ControlWidget { 22 | var body: some ControlWidgetConfiguration { 23 | StaticControlConfiguration( 24 | kind: "com.juniperphoton.widget.control" 25 | ) { 26 | ControlWidgetButton(action: AppCaptureIntent()) { 27 | Label("Launch App", systemImage: "camera.shutter.button") 28 | } 29 | } 30 | .displayName("Launch Camera") 31 | .description("A an example control that launch camera.") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Packages/Models/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Models", 8 | platforms: [ 9 | .iOS(.v16), 10 | .macOS(.v13) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, making them visible to other packages. 14 | .library( 15 | name: "Models", 16 | targets: ["Models"]), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package, defining a module or a test suite. 20 | // Targets can depend on other targets in this package and products from dependencies. 21 | .target( 22 | name: "Models"), 23 | .testTarget( 24 | name: "ModelsTests", 25 | dependencies: ["Models"] 26 | ), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/Camera/AppUserDefaultSettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppUserDefaultSettings.swift 3 | // LockedCameraCaptureExtensionDemo 4 | // 5 | // Created by Photon Juniper on 2024/8/21. 6 | // 7 | import SwiftUI 8 | import Models 9 | 10 | class AppUserDefaultSettings { 11 | static let shared = AppUserDefaultSettings() 12 | 13 | enum Keys: String { 14 | case cameraPosition 15 | } 16 | 17 | var cameraPosition: CameraPosition { 18 | get { 19 | if let rawValue = UserDefaults.standard.string(forKey: AppUserDefaultSettings.Keys.cameraPosition.rawValue) { 20 | return CameraPosition(rawValue: rawValue) ?? .back 21 | } else { 22 | return .back 23 | } 24 | } 25 | set { 26 | UserDefaults.standard.setValue(newValue.rawValue, forKey: AppUserDefaultSettings.Keys.cameraPosition.rawValue) 27 | } 28 | } 29 | 30 | private init() { 31 | // empty 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LockedExtension/LockedExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LockedExtension.swift 3 | // LockedExtension 4 | // 5 | // Created by Photon Juniper on 2024/8/20. 6 | // 7 | 8 | import Foundation 9 | import LockedCameraCapture 10 | import SwiftUI 11 | 12 | @main 13 | struct LockedExtension: LockedCameraCaptureExtension { 14 | var body: some LockedCameraCaptureExtensionScene { 15 | LockedCameraCaptureUIScene { session in 16 | LockedCameraCaptureView(session: session) 17 | } 18 | } 19 | } 20 | 21 | struct LockedCameraCaptureView: View { 22 | let session: LockedCameraCaptureSession 23 | 24 | var body: some View { 25 | // In LockedCameraCaptureExtensionScene, scenePhase will not be active. 26 | // Thus we need to set the scenePhase to active manually. 27 | ContentView(configProvider: AppStorageConfigProvider(session)) 28 | .environment(\.scenePhase, .active) 29 | .environment(\.openMainApp, OpenMainAppAction(session: session)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Packages/MetalLib/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MetalLib", 8 | platforms: [ 9 | .iOS(.v16), 10 | .macOS(.v13) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries a package produces, making them visible to other packages. 14 | .library( 15 | name: "MetalLib", 16 | targets: ["MetalLib"]), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package, defining a module or a test suite. 20 | // Targets can depend on other targets in this package and products from dependencies. 21 | .target( 22 | name: "MetalLib"), 23 | .testTarget( 24 | name: "MetalLibTests", 25 | dependencies: ["MetalLib"] 26 | ), 27 | ], 28 | swiftLanguageModes: [.v5] 29 | ) 30 | -------------------------------------------------------------------------------- /LockedExtension/OpenMainAppButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenMainAppButton.swift 3 | // LockedCameraCaptureExtensionDemo 4 | // 5 | // Created by Photon Juniper on 2024/8/21. 6 | // 7 | import LockedCameraCapture 8 | import SwiftUI 9 | 10 | @available(iOS 18, *) 11 | struct OpenMainAppButton: View { 12 | @Environment(\.openMainApp) private var openMainApp: OpenMainAppAction 13 | 14 | var body: some View { 15 | Button { 16 | // For unknown reasons, we can't use the NSUserActivityTypeLockedCameraCapture constant 17 | // if we don't set the minimum target to iOS 18. 18 | // Otherwise when the app runs on the previous version, it will crash. 19 | openMainApp(NSUserActivity(activityType: NSUserActivityTypeLockedCameraCapture)) 20 | } label: { 21 | Label("OPEN MAIN APP", systemImage: "arrow.up.right") 22 | .padding(4) 23 | .foregroundStyle(.black) 24 | .font(.footnote.bold()) 25 | .background(RoundedRectangle(cornerRadius: 4).fill(.yellow)) 26 | }.buttonStyle(.plain) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Packages/MetalLib/Sources/MetalLib/ViewRepresentable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewRepresentable.swift 3 | // PhotonCam 4 | // 5 | // Created by Photon Juniper on 2023/10/27. 6 | // 7 | import SwiftUI 8 | 9 | #if os(iOS) || os(tvOS) 10 | public protocol ViewRepresentable: UIViewRepresentable { 11 | associatedtype ViewType = UIViewType 12 | func makeView(context: Context) -> ViewType 13 | func updateView(_ view: ViewType, context: Context) 14 | } 15 | 16 | extension ViewRepresentable { 17 | public func makeUIView(context: Context) -> ViewType { 18 | makeView(context: context) 19 | } 20 | 21 | public func updateUIView(_ uiView: ViewType, context: Context) { 22 | updateView(uiView, context: context) 23 | } 24 | } 25 | #elseif os(macOS) 26 | public protocol ViewRepresentable: NSViewRepresentable { 27 | associatedtype ViewType = NSViewType 28 | func makeView(context: Context) -> ViewType 29 | func updateView(_ view: ViewType, context: Context) 30 | } 31 | 32 | extension ViewRepresentable { 33 | public func makeNSView(context: Context) -> ViewType { 34 | makeView(context: context) 35 | } 36 | 37 | public func updateNSView(_ nsView: ViewType, context: Context) { 38 | updateView(nsView, context: context) 39 | } 40 | } 41 | #endif 42 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo.xcodeproj/xcuserdata/juniperphoton.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | IntentsLibFramework.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 3 11 | 12 | LockedCameraCaptureExtensionDemo.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 2 16 | 17 | LockedExtension.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 0 21 | 22 | LockedWidgetExtension.xcscheme_^#shared#^_ 23 | 24 | orderHint 25 | 1 26 | 27 | 28 | SuppressBuildableAutocreation 29 | 30 | 22DE8DDA2C74B36800FC6EEA 31 | 32 | primary 33 | 34 | 35 | 22DE8DF02C74B3CA00FC6EEA 36 | 37 | primary 38 | 39 | 40 | 22DE8E032C74B3E500FC6EEA 41 | 42 | primary 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/Camera/OpenMainAppAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OpenMainAppAction.swift 3 | // LockedCameraCaptureExtensionDemo 4 | // 5 | // Created by Photon Juniper on 2024/8/21. 6 | // 7 | import SwiftUI 8 | import LockedCameraCapture 9 | 10 | struct OpenMainAppAction { 11 | private var action: (NSUserActivity) -> Void 12 | 13 | init() { 14 | self.init { _ in 15 | // ignored 16 | } 17 | } 18 | 19 | @available(iOS 18, *) 20 | init(session: LockedCameraCaptureSession) { 21 | self.init { activity in 22 | Task { 23 | do { 24 | try await session.openApplication(for: activity) 25 | } catch { 26 | print("failed to open application \(error)") 27 | } 28 | } 29 | } 30 | } 31 | 32 | init(action: @escaping (NSUserActivity) -> Void) { 33 | self.action = action 34 | } 35 | 36 | func callAsFunction(_ activity: NSUserActivity) { 37 | self.action(activity) 38 | } 39 | } 40 | 41 | struct OpenMainAppActionKey: EnvironmentKey { 42 | static var defaultValue: OpenMainAppAction = OpenMainAppAction() 43 | } 44 | 45 | extension EnvironmentValues { 46 | var openMainApp: OpenMainAppAction { 47 | get { self[OpenMainAppActionKey.self] } 48 | set { self[OpenMainAppActionKey.self] = newValue } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/Camera/ViewModel/CamPreviewViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CamPreviewViewModel.swift 3 | // LockedCameraCaptureExtensionDemo 4 | // 5 | // Created by Photon Juniper on 2024/8/21. 6 | // 7 | import SwiftUI 8 | import AVFoundation 9 | import MetalLib 10 | 11 | class CamPreviewViewModel: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDelegate { 12 | @Published private(set) var previewImage: CIImage? = nil 13 | @Published private(set) var renderer = MetalRenderer() 14 | 15 | private(set) var previewQueue = DispatchQueue(label: "preview_queue") 16 | 17 | func initializeRenderer() { 18 | renderer.initializeCIContext(colorSpace: nil, name: "preview") 19 | } 20 | 21 | @MainActor 22 | func updatePreviewImage(_ previewImage: CIImage?) { 23 | self.previewImage = previewImage 24 | self.renderer.requestChanged(displayedImage: previewImage) 25 | } 26 | 27 | func captureOutput( 28 | _ output: AVCaptureOutput, 29 | didOutput sampleBuffer: CMSampleBuffer, 30 | from connection: AVCaptureConnection 31 | ) { 32 | let image = getVideoOutputImage(output, didOutput: sampleBuffer) 33 | DispatchQueue.main.async { 34 | self.updatePreviewImage(image) 35 | } 36 | } 37 | 38 | private func getVideoOutputImage( 39 | _ output: AVCaptureOutput, 40 | didOutput sampleBuffer: CMSampleBuffer 41 | ) -> CIImage? { 42 | guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { 43 | return nil 44 | } 45 | return CIImage(cvPixelBuffer: pixelBuffer) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/Intent/AppCaptureIntent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyAppCaptureIntent.swift 3 | // LockedCameraCaptureExtensionDemo 4 | // 5 | // Created by Photon Juniper on 2024/8/20. 6 | // 7 | import AppIntents 8 | import AVFoundation 9 | import Models 10 | 11 | /// The implementation of CameraCaptureIntent should always built 12 | /// against the App and the extension targets, otherwise it won't be discovered as expected. 13 | /// 14 | /// If you own your framework, it possible that you put this implementation in the framework 15 | /// and set the target membership to be included in the App and the extension targets, even if 16 | /// the source code is in the framework itself. 17 | /// 18 | /// Note: it looks like there is a specific API [AppIntentsPackage](https://developer.apple.com/documentation/appintents/appintentspackage) 19 | /// that should solve this discovery issue. But it won't work. 20 | /// 21 | /// There is a post about this issue: 22 | /// [AppIntentsPackage protocol with SPM package not working](https://forums.developer.apple.com/forums/thread/732535). 23 | @available(iOS 18, *) 24 | struct AppCaptureIntent: CameraCaptureIntent { 25 | struct MyAppContext: Codable, Sendable { 26 | public private(set) var cameraPosition: CameraPosition = .back 27 | 28 | public init(cameraPosition: CameraPosition) { 29 | self.cameraPosition = cameraPosition 30 | } 31 | } 32 | 33 | typealias AppContext = MyAppContext 34 | 35 | static let title: LocalizedStringResource = "AppCaptureIntent" 36 | static let description = IntentDescription("Capture photos with MyApp.") 37 | 38 | @MainActor 39 | func perform() async throws -> some IntentResult { 40 | return .result() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "AppCaptureIntent" : { 5 | 6 | }, 7 | "Capture photos with MyApp." : { 8 | 9 | }, 10 | "NoPermissionHint" : { 11 | "extractionState" : "manual", 12 | "localizations" : { 13 | "en" : { 14 | "stringUnit" : { 15 | "state" : "translated", 16 | "value" : "You have denied Photo Library or Camera permission, please grant them in the system's settings." 17 | } 18 | }, 19 | "zh-Hans" : { 20 | "stringUnit" : { 21 | "state" : "translated", 22 | "value" : "你拒绝了相机权限。请到设置里授予该权限。" 23 | } 24 | }, 25 | "zh-Hant" : { 26 | "stringUnit" : { 27 | "state" : "translated", 28 | "value" : "你拒絕了相機權限。請到設置里授予該權限。" 29 | } 30 | } 31 | } 32 | }, 33 | "OPEN MAIN APP" : { 34 | 35 | }, 36 | "TapToCaptureHint" : { 37 | "extractionState" : "manual", 38 | "localizations" : { 39 | "en" : { 40 | "stringUnit" : { 41 | "state" : "translated", 42 | "value" : "You can tap the button below to capture a photo or use Volume buttons or Camera Control to capture." 43 | } 44 | }, 45 | "zh-Hans" : { 46 | "stringUnit" : { 47 | "state" : "translated", 48 | "value" : "你可以点击下方的拍照按钮或者使用音量键或相机控制来进行拍照。" 49 | } 50 | }, 51 | "zh-Hant" : { 52 | "stringUnit" : { 53 | "state" : "translated", 54 | "value" : "你可以點擊下方的拍照按鈕或者使用音量鍵或相機控制來進行拍照。" 55 | } 56 | } 57 | } 58 | } 59 | }, 60 | "version" : "1.0" 61 | } -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/Camera/View/CaptureInteractionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InteractionView.swift 3 | // LockedCameraCaptureExtensionDemo 4 | // 5 | // Created by Photon Juniper on 2024/8/20. 6 | // 7 | import SwiftUI 8 | import Photos 9 | import AVFoundation 10 | import AVKit 11 | 12 | extension View { 13 | @ViewBuilder 14 | func onPressCapture(action: @escaping () -> Void) -> some View { 15 | if #available(iOS 18.0, *) { 16 | self.onCameraCaptureEvent { event in 17 | switch event.phase { 18 | case .ended: 19 | action() 20 | default: 21 | break 22 | } 23 | } secondaryAction: { event in 24 | switch event.phase { 25 | case .ended: 26 | action() 27 | default: 28 | break 29 | } 30 | } 31 | } else if #available(iOS 17.2, *) { 32 | self.background { 33 | CaptureInteractionView(action: action) 34 | } 35 | } else { 36 | self 37 | } 38 | } 39 | } 40 | 41 | @available(iOS 17.2, *) 42 | private struct CaptureInteractionView: UIViewRepresentable { 43 | var action: () -> Void 44 | 45 | func makeUIView(context: Context) -> some UIView { 46 | let uiView = UIView() 47 | let interaction = AVCaptureEventInteraction { event in 48 | if event.phase == .began { 49 | action() 50 | } 51 | } 52 | 53 | uiView.addInteraction(interaction) 54 | return uiView 55 | } 56 | 57 | func updateUIView(_ uiView: UIViewType, context: Context) { 58 | // ignored 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | }, 30 | { 31 | "idiom" : "mac", 32 | "scale" : "1x", 33 | "size" : "16x16" 34 | }, 35 | { 36 | "idiom" : "mac", 37 | "scale" : "2x", 38 | "size" : "16x16" 39 | }, 40 | { 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "32x32" 44 | }, 45 | { 46 | "idiom" : "mac", 47 | "scale" : "2x", 48 | "size" : "32x32" 49 | }, 50 | { 51 | "idiom" : "mac", 52 | "scale" : "1x", 53 | "size" : "128x128" 54 | }, 55 | { 56 | "idiom" : "mac", 57 | "scale" : "2x", 58 | "size" : "128x128" 59 | }, 60 | { 61 | "idiom" : "mac", 62 | "scale" : "1x", 63 | "size" : "256x256" 64 | }, 65 | { 66 | "idiom" : "mac", 67 | "scale" : "2x", 68 | "size" : "256x256" 69 | }, 70 | { 71 | "idiom" : "mac", 72 | "scale" : "1x", 73 | "size" : "512x512" 74 | }, 75 | { 76 | "idiom" : "mac", 77 | "scale" : "2x", 78 | "size" : "512x512" 79 | } 80 | ], 81 | "info" : { 82 | "author" : "xcode", 83 | "version" : 1 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Packages/MetalLib/Sources/MetalLib/MetalView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetalView.swift 3 | // PhotonCam 4 | // 5 | // Created by Photon Juniper on 2023/10/27. 6 | // 7 | import SwiftUI 8 | import MetalKit 9 | 10 | private class BoundAwareMTKView: MTKView { 11 | private var currentBounds: CGRect = .zero 12 | 13 | var onBoundsChanged: ((CGRect) -> Void)? = nil 14 | 15 | #if canImport(UIKit) 16 | override func layoutSubviews() { 17 | super.layoutSubviews() 18 | 19 | if self.currentBounds != self.bounds { 20 | self.currentBounds = self.bounds 21 | onBoundsChanged?(self.currentBounds) 22 | } 23 | } 24 | #else 25 | override func layout() { 26 | super.layout() 27 | 28 | if self.currentBounds != self.bounds { 29 | self.currentBounds = self.bounds 30 | onBoundsChanged?(self.currentBounds) 31 | } 32 | } 33 | #endif 34 | } 35 | 36 | public struct MetalView: ViewRepresentable { 37 | @ObservedObject public var renderer: MetalRenderer 38 | 39 | public let enableSetNeedsDisplay: Bool 40 | public let isOpaque: Bool 41 | 42 | public init(renderer: MetalRenderer, enableSetNeedsDisplay: Bool, isOpaque: Bool = true) { 43 | self.renderer = renderer 44 | self.enableSetNeedsDisplay = enableSetNeedsDisplay 45 | self.isOpaque = isOpaque 46 | } 47 | 48 | /// - Tag: MakeView 49 | public func makeView(context: Context) -> MTKView { 50 | let view = BoundAwareMTKView(frame: .zero, device: renderer.device) 51 | view.onBoundsChanged = { [weak view] bounds in 52 | if enableSetNeedsDisplay { 53 | view?.setNeedsDisplay(bounds) 54 | } 55 | } 56 | 57 | if enableSetNeedsDisplay { 58 | view.enableSetNeedsDisplay = true 59 | view.isPaused = true 60 | } else { 61 | // Suggest to Core Animation, through MetalKit, how often to redraw the view. 62 | view.preferredFramesPerSecond = 30 63 | view.enableSetNeedsDisplay = false 64 | view.isPaused = false 65 | } 66 | 67 | // Allow Core Image to render to the view using the Metal compute pipeline. 68 | view.framebufferOnly = false 69 | view.delegate = renderer 70 | 71 | if let layer = view.layer as? CAMetalLayer { 72 | layer.isOpaque = isOpaque 73 | } 74 | 75 | return view 76 | } 77 | 78 | public func updateView(_ view: MTKView, context: Context) { 79 | configure(view: view, using: renderer) 80 | view.setNeedsDisplay(view.bounds) 81 | } 82 | 83 | private func configure(view: MTKView, using renderer: MetalRenderer) { 84 | view.delegate = renderer 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/Camera/ViewModel/CaptureProcessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptureProcessor.swift 3 | // LockedCameraCaptureExtensionDemo 4 | // 5 | // Created by Photon Juniper on 2024/8/21. 6 | // 7 | import AVFoundation 8 | import Photos 9 | 10 | class CaptureProcessor: NSObject, ObservableObject, AVCapturePhotoCaptureDelegate { 11 | @Published var saveResultText: String = "" 12 | 13 | let configProvider: AppStorageConfigProvider 14 | 15 | init(configProvider: AppStorageConfigProvider) { 16 | self.configProvider = configProvider 17 | } 18 | 19 | func photoOutput( 20 | _ output: AVCapturePhotoOutput, 21 | didFinishProcessingPhoto photo: AVCapturePhoto, 22 | error: (any Error)? 23 | ) { 24 | if let error = error { 25 | print("photoOutput didFinishProcessingPhoto error \(error)") 26 | return 27 | } 28 | 29 | Task { @MainActor in 30 | await savePhotoToLibrary(photo) 31 | } 32 | } 33 | 34 | @MainActor 35 | private func savePhotoToLibrary(_ photo: AVCapturePhoto) async { 36 | let saved = await savePhotoToLibraryInternal(photo) 37 | if saved { 38 | saveResultText = "Saved to Photo Library" 39 | } else { 40 | saveResultText = "Failed to save, see the console for more details" 41 | } 42 | 43 | do { 44 | try await Task.sleep(for: .seconds(2)) 45 | saveResultText = "" 46 | } catch { 47 | // ignored 48 | } 49 | } 50 | 51 | private nonisolated func savePhotoToLibraryInternal(_ photo: AVCapturePhoto) async -> Bool { 52 | guard let photoData = photo.fileDataRepresentation() else { 53 | print("can't get photoData") 54 | return false 55 | } 56 | 57 | // Saving the data to a file isn't strictly necessary since 58 | // PHAssetCreationRequest can accept Data directly. 59 | // However, this example demonstrates saving the photo data to a temporary file 60 | // using a container URL provided by the environment. 61 | guard let fileURL = configProvider.rootURL?.appendingPathComponent( 62 | UUID().uuidString, 63 | conformingTo: .heic 64 | ) else { 65 | print("can't get fileURL") 66 | return false 67 | } 68 | 69 | print("savePhotoToLibraryInternal, fileURL is \(String(describing: fileURL))") 70 | 71 | do { 72 | try photoData.write(to: fileURL) 73 | } catch { 74 | print("savePhotoToLibraryInternal, failed to write to file \(error)") 75 | return false 76 | } 77 | 78 | return await withCheckedContinuation { continuation in 79 | PHPhotoLibrary.shared().performChanges { 80 | let _ = PHAssetCreationRequest.creationRequestForAssetFromImage(atFileURL: fileURL) 81 | } completionHandler: { success, error in 82 | print("savePhotoToLibrary, success: \(success), error: \(String(describing: error))") 83 | continuation.resume(returning: success) 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo.xcodeproj/xcshareddata/xcschemes/LockedCameraCaptureExtensionDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo.xcodeproj/xcshareddata/xcschemes/LockedExtension.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 11 | 17 | 23 | 24 | 25 | 31 | 37 | 38 | 39 | 40 | 41 | 47 | 48 | 60 | 62 | 68 | 69 | 70 | 71 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo.xcodeproj/xcshareddata/xcschemes/LockedWidgetExtension.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 11 | 17 | 23 | 24 | 25 | 31 | 37 | 38 | 39 | 40 | 41 | 47 | 48 | 60 | 62 | 68 | 69 | 70 | 71 | 75 | 76 | 80 | 81 | 85 | 86 | 87 | 88 | 96 | 98 | 104 | 105 | 106 | 107 | 109 | 110 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/Camera/View/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // LockedCameraCaptureExtensionDemo 4 | // 5 | // Created by Photon Juniper on 2024/8/20. 6 | // 7 | 8 | import SwiftUI 9 | import MetalLib 10 | 11 | struct ContentView: View { 12 | @Environment(\.scenePhase) private var scenePhase 13 | 14 | @StateObject private var previewViewModel: CamPreviewViewModel 15 | @StateObject private var viewModel: MainViewModel 16 | @StateObject private var captureProcessor: CaptureProcessor 17 | 18 | /// Construct ``ContentView`` given the instance of ``AppStorageConfigProvider``, which provides the information 19 | /// about the current environment. 20 | init(configProvider: AppStorageConfigProvider) { 21 | let previewViewModel = CamPreviewViewModel() 22 | let captureProcessor = CaptureProcessor(configProvider: configProvider) 23 | let mainViewModel = MainViewModel( 24 | camPreviewViewModel: previewViewModel, 25 | captureProcessor: captureProcessor 26 | ) 27 | 28 | self._previewViewModel = StateObject(wrappedValue: previewViewModel) 29 | self._captureProcessor = StateObject(wrappedValue: captureProcessor) 30 | self._viewModel = StateObject(wrappedValue: mainViewModel) 31 | } 32 | 33 | var body: some View { 34 | VStack(spacing: 0) { 35 | if viewModel.showNoPermissionHint { 36 | Text("NoPermissionHint") 37 | } else { 38 | // This demo uses MTKView to render CIImage, showing the possibility to not use AVCaptureVideoPreviewLayer. 39 | MetalView(renderer: previewViewModel.renderer, enableSetNeedsDisplay: true) 40 | .overlay { 41 | if viewModel.showFlashScreen { 42 | Color.black.zIndex(1) 43 | } 44 | } 45 | 46 | VStack { 47 | #if CAPTURE_EXTENSION 48 | OpenMainAppButton() 49 | .padding(.bottom) 50 | #endif 51 | Text("TapToCaptureHint") 52 | .foregroundStyle(.white) 53 | .font(.footnote) 54 | .padding() 55 | 56 | ZStack { 57 | SwitchCameraPositionButton(viewModel: viewModel) 58 | .frame(maxWidth: .infinity, alignment: .trailing) 59 | 60 | CaptureButton(viewModel: viewModel) 61 | } 62 | } 63 | .padding() 64 | .disabled(viewModel.isSettingUpCamera) 65 | .opacity(viewModel.isSettingUpCamera ? 0.5 : 1.0) 66 | } 67 | } 68 | .overlay { 69 | if !captureProcessor.saveResultText.isEmpty { 70 | Text(captureProcessor.saveResultText) 71 | .padding(12) 72 | .foregroundStyle(.black) 73 | .background(RoundedRectangle(cornerRadius: 12).fill(.white)) 74 | .frame(maxHeight: .infinity, alignment: .top) 75 | .padding(12) 76 | } 77 | } 78 | .background { 79 | Color.black.ignoresSafeArea() 80 | } 81 | .animation(.default, value: viewModel.isSettingUpCamera) 82 | .animation(.default, value: captureProcessor.saveResultText) 83 | .onPressCapture { 84 | Task { 85 | await viewModel.capturePhoto() 86 | } 87 | } 88 | .task(id: scenePhase) { 89 | switch scenePhase { 90 | case .background: 91 | await viewModel.stopCamera() 92 | case .active: 93 | await viewModel.updateFromAppContext() 94 | await viewModel.setup() 95 | default: 96 | break 97 | } 98 | } 99 | .task { 100 | previewViewModel.initializeRenderer() 101 | } 102 | } 103 | } 104 | 105 | private struct SwitchCameraPositionButton: View { 106 | @ObservedObject var viewModel: MainViewModel 107 | 108 | var body: some View { 109 | Button { 110 | Task { 111 | await viewModel.toggleCameraPositionSwitch() 112 | } 113 | } label: { 114 | Image(systemName: "arrow.triangle.2.circlepath.camera") 115 | .padding() 116 | .foregroundStyle(viewModel.cameraPosition == .back ? .white : .black) 117 | .background { 118 | Circle().fill(Color.white.opacity(viewModel.cameraPosition == .back ? 0.1 : 1.0)) 119 | } 120 | }.buttonStyle(.plain) 121 | } 122 | } 123 | 124 | private struct CaptureButton: View { 125 | @ObservedObject var viewModel: MainViewModel 126 | 127 | var body: some View { 128 | Button { 129 | Task { 130 | await viewModel.capturePhoto() 131 | } 132 | } label: { 133 | Circle().fill(Color.white) 134 | }.buttonStyle(.plain) 135 | .frame(width: 80, height: 80) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo/Camera/ViewModel/MainViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewModel.swift 3 | // LockedCameraCaptureExtensionDemo 4 | // 5 | // Created by Photon Juniper on 2024/8/20. 6 | // 7 | import SwiftUI 8 | import AVFoundation 9 | import Photos 10 | import LockedCameraCapture 11 | import Models 12 | 13 | class MainViewModel: ObservableObject { 14 | @Published var showNoPermissionHint: Bool = false 15 | @Published var cameraPosition: CameraPosition = .back { 16 | didSet { 17 | Task { 18 | await updateAppContext() 19 | } 20 | } 21 | } 22 | @Published var isSettingUpCamera = false 23 | @Published var showFlashScreen = false 24 | 25 | private var session: AVCaptureSession? = nil 26 | private var photoOutput: AVCapturePhotoOutput? = nil 27 | 28 | private let camPreviewViewModel: CamPreviewViewModel 29 | private let captureProcessor: CaptureProcessor 30 | 31 | init(camPreviewViewModel: CamPreviewViewModel, captureProcessor: CaptureProcessor) { 32 | self.camPreviewViewModel = camPreviewViewModel 33 | self.captureProcessor = captureProcessor 34 | } 35 | 36 | @MainActor 37 | func setup() async { 38 | guard await requestForPermission() else { 39 | showNoPermissionHint = true 40 | return 41 | } 42 | 43 | await setupInternal() 44 | } 45 | 46 | @MainActor 47 | func capturePhoto() async { 48 | guard let photoOutput = photoOutput else { 49 | print("can't find photo output") 50 | return 51 | } 52 | 53 | let settings = AVCapturePhotoSettings() 54 | photoOutput.capturePhoto(with: settings, delegate: captureProcessor) 55 | 56 | self.showFlashScreen = true 57 | try? await Task.sleep(for: .seconds(0.2)) 58 | self.showFlashScreen = false 59 | } 60 | 61 | @MainActor 62 | func toggleCameraPositionSwitch() async { 63 | self.cameraPosition = self.cameraPosition == .back ? .front : .back 64 | await reconfigureCamera() 65 | } 66 | 67 | @MainActor 68 | func reconfigureCamera() async { 69 | await stopCamera() 70 | await setupInternal() 71 | } 72 | 73 | @MainActor 74 | func stopCamera() async { 75 | guard let cameraSession = self.session else { 76 | return 77 | } 78 | 79 | cameraSession.stopRunning() 80 | self.session = nil 81 | } 82 | 83 | @MainActor 84 | private func setupInternal() async { 85 | if isSettingUpCamera { 86 | print("isSettingUpCamera, skip") 87 | return 88 | } 89 | 90 | isSettingUpCamera = true 91 | 92 | defer { 93 | isSettingUpCamera = false 94 | } 95 | 96 | print("start setting up") 97 | 98 | guard let (cameraSession, photoOutput) = await setupCameraSession(position: cameraPosition) else { 99 | return 100 | } 101 | 102 | self.session = cameraSession 103 | self.photoOutput = photoOutput 104 | } 105 | 106 | private nonisolated func setupCameraSession(position: CameraPosition) async -> (AVCaptureSession, AVCapturePhotoOutput)? { 107 | do { 108 | let session = AVCaptureSession() 109 | session.beginConfiguration() 110 | session.sessionPreset = .photo 111 | 112 | guard let device = AVCaptureDevice.default( 113 | .builtInWideAngleCamera, 114 | for: .video, 115 | position: position.avFoundationPosition 116 | ) else { 117 | print("can't find AVCaptureDevice") 118 | return nil 119 | } 120 | 121 | session.addInput(try AVCaptureDeviceInput(device: device)) 122 | 123 | let videoOutput = AVCaptureVideoDataOutput() 124 | videoOutput.setSampleBufferDelegate(camPreviewViewModel, queue: camPreviewViewModel.previewQueue) 125 | 126 | session.addOutput(videoOutput) 127 | 128 | let photoOutput = AVCapturePhotoOutput() 129 | session.addOutput(photoOutput) 130 | 131 | if let connection = videoOutput.connection(with: .video) { 132 | if connection.isVideoRotationAngleSupported(90) { 133 | connection.videoRotationAngle = 90 134 | } 135 | if connection.isVideoMirroringSupported && position == .front { 136 | connection.isVideoMirrored = true 137 | } 138 | } 139 | 140 | session.commitConfiguration() 141 | session.startRunning() 142 | 143 | return (session, photoOutput) 144 | } catch { 145 | print("error while setting up camera \(error)") 146 | return nil 147 | } 148 | } 149 | 150 | private func requestForPermission() async -> Bool { 151 | let photoLibraryPermissionResult = await PHPhotoLibrary.requestAuthorization(for: .addOnly) 152 | let cameraPermissionPermitted = await AVCaptureDevice.requestAccess(for: .video) 153 | return photoLibraryPermissionResult == .authorized && cameraPermissionPermitted 154 | } 155 | 156 | @MainActor 157 | private func updateAppContext() async { 158 | #if !CAPTURE_EXTENSION 159 | AppUserDefaultSettings.shared.cameraPosition = self.cameraPosition 160 | #endif 161 | 162 | if #available(iOS 18, *) { 163 | let appContext = AppCaptureIntent.AppContext(cameraPosition: cameraPosition) 164 | 165 | do { 166 | try await AppCaptureIntent.updateAppContext(appContext) 167 | print("app context updated") 168 | } catch { 169 | print("error on updating app context \(error)") 170 | } 171 | } 172 | } 173 | 174 | @MainActor 175 | func updateFromAppContext() async { 176 | #if !CAPTURE_EXTENSION 177 | // If it's in the main app, first read the value from the UserDefaults. 178 | self.cameraPosition = AppUserDefaultSettings.shared.cameraPosition 179 | #endif 180 | 181 | if #available(iOS 18, *) { 182 | do { 183 | // If `AppCaptureIntent.appContext` exists, then read from it. 184 | if let appContext = try await AppCaptureIntent.appContext { 185 | self.cameraPosition = appContext.cameraPosition 186 | print("updated from app context") 187 | } else { 188 | print("app context is nil") 189 | } 190 | } catch { 191 | print("error on getting app context") 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /Packages/MetalLib/Sources/MetalLib/MetalRenderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Renderer.swift 3 | // PhotonCam 4 | // 5 | // Created by Photon Juniper on 2023/10/27. 6 | // 7 | 8 | import Foundation 9 | import Metal 10 | import MetalKit 11 | import CoreImage 12 | 13 | private let maxBuffersInFlight = 3 14 | 15 | public final class MetalRenderer: NSObject, MTKViewDelegate, ObservableObject { 16 | @Published private(set) var requestedDisplayedTime = CFAbsoluteTimeGetCurrent() 17 | 18 | public let device: MTLDevice 19 | 20 | private let commandQueue: MTLCommandQueue 21 | private var ciContext: CIContext? = nil 22 | private var opaqueBackground: CIImage 23 | private let startTime: CFAbsoluteTime 24 | 25 | private let inFlightSemaphore = DispatchSemaphore(value: maxBuffersInFlight) 26 | private(set) var scaleToFill: Bool = false 27 | 28 | private var displayedImage: CIImage? = nil 29 | private var queue: DispatchQueue? = nil 30 | 31 | private var clearDestination: Bool = false 32 | 33 | public override init() { 34 | let start = CFAbsoluteTimeGetCurrent() 35 | self.device = MTLCreateSystemDefaultDevice()! 36 | self.commandQueue = self.device.makeCommandQueue()! 37 | self.opaqueBackground = CIImage.black 38 | 39 | self.startTime = CFAbsoluteTimeGetCurrent() 40 | 41 | debugPrint("MetalRenderer init \(CFAbsoluteTimeGetCurrent() - start)s") 42 | super.init() 43 | } 44 | 45 | /// The the background color to be composited over with. 46 | /// If the color is not opaque, please remember to set ``isOpaque`` in ``MetalView``. 47 | public func setBackgroundColor(ciColor: CIColor) { 48 | setBackgroundImage(CIImage(color: ciColor)) 49 | } 50 | 51 | /// The the background image to be composited over with. 52 | public func setBackgroundImage(_ image: CIImage) { 53 | self.opaqueBackground = image 54 | } 55 | 56 | public func setScaleToFill(scaleToFill: Bool) { 57 | self.scaleToFill = scaleToFill 58 | } 59 | 60 | /// Initialize the CIContext with a specified working ``CGColorSpace``. 61 | /// - parameter colorSpace: The working Color Space to use. 62 | /// - parameter queue: A dedicated queue to start the render task. Although the whole render process is run by GPU, 63 | /// however creating and submitting textures to GPU will introduce performance overhead, and it's recommended not to do it on main thread. 64 | public func initializeCIContext( 65 | colorSpace: CGColorSpace?, 66 | name: String, 67 | queue: DispatchQueue? = DispatchQueue(label: "metal_render_queue", qos: .userInitiated) 68 | ) { 69 | self.queue = queue 70 | 71 | let start = CFAbsoluteTimeGetCurrent() 72 | 73 | // Set up the Core Image context's options: 74 | // - Name the context to make CI_PRINT_TREE debugging easier. 75 | // - Disable caching because the image differs every frame. 76 | // - Allow the context to use the low-power GPU, if available. 77 | var options = [CIContextOption: Any]() 78 | options = [ 79 | .name: name, 80 | .cacheIntermediates: false, 81 | .allowLowPower: true, 82 | ] 83 | if let colorSpace = colorSpace { 84 | options[.workingColorSpace] = colorSpace 85 | } 86 | 87 | self.ciContext = CIContext( 88 | mtlCommandQueue: self.commandQueue, 89 | options: options 90 | ) 91 | 92 | print("MetalRenderer initializeCIContext \(CFAbsoluteTimeGetCurrent() - start)s, name: \(name) to color space: \(String(describing: colorSpace))") 93 | } 94 | 95 | /// Request update the image. 96 | /// - parameter displayedImage: The CIImage to be rendered. 97 | public func requestChanged(displayedImage: CIImage?) { 98 | self.displayedImage = displayedImage 99 | self.requestedDisplayedTime = CFAbsoluteTimeGetCurrent() 100 | } 101 | 102 | /// Request clear the destination. 103 | /// - parameter clearDestination: If the destination should be cleared first before starting a new task to render image. 104 | public func requestClearDestination(clearDestination: Bool) { 105 | self.clearDestination = clearDestination 106 | } 107 | 108 | // MARK: MTKViewDelegate 109 | public func draw(in view: MTKView) { 110 | drawInternal(view) 111 | } 112 | 113 | public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { 114 | // Respond to drawable size or orientation changes. 115 | } 116 | 117 | /// Draw the content into the view's texture. 118 | private func drawInternal(_ view: MTKView) { 119 | guard let ciContext = self.ciContext else { 120 | print("CIContext is nil!") 121 | return 122 | } 123 | 124 | // Create a displayable image for the current time. 125 | guard var image = self.displayedImage else { 126 | return 127 | } 128 | 129 | _ = self.inFlightSemaphore.wait(timeout: DispatchTime.distantFuture) 130 | if let commandBuffer = self.commandQueue.makeCommandBuffer() { 131 | 132 | // Add a completion handler that signals `inFlightSemaphore` when Metal and the GPU have fully 133 | // finished processing the commands that the app encoded for this frame. 134 | // This completion indicates that Metal and the GPU no longer need the dynamic buffers that 135 | // Core Image writes to in this frame. 136 | // Therefore, the CPU can overwrite the buffer contents without corrupting any rendering operations. 137 | let semaphore = self.inFlightSemaphore 138 | commandBuffer.addCompletedHandler { (_ commandBuffer)-> Swift.Void in 139 | semaphore.signal() 140 | } 141 | 142 | if let drawable = view.currentDrawable { 143 | let dSize = view.drawableSize 144 | 145 | // Create a destination the Core Image context uses to render to the drawable's Metal texture. 146 | let destination = CIRenderDestination( 147 | width: Int(dSize.width), 148 | height: Int(dSize.height), 149 | pixelFormat: view.colorPixelFormat, 150 | commandBuffer: nil 151 | ) { 152 | return drawable.texture 153 | } 154 | 155 | let scaleW = CGFloat(dSize.width) / image.extent.width 156 | let scaleH = CGFloat(dSize.height) / image.extent.height 157 | 158 | // To perform scaledToFit, use min. Use max for scaledToFill effect. 159 | let scale: CGFloat 160 | if self.scaleToFill { 161 | scale = max(scaleW, scaleH) 162 | } else { 163 | scale = min(scaleW, scaleH) 164 | } 165 | 166 | let originalExtent = image.extent 167 | let scaledWidth = Int(originalExtent.width * scale) 168 | let scaledHeight = Int(originalExtent.height * scale) 169 | image = image.transformed(by: CGAffineTransform(scaleX: scale, y: scale)) 170 | image = image.cropped(to: CGRect(x: 0, y: 0, width: scaledWidth, height: scaledHeight)) 171 | 172 | // Center the image in the view's visible area. 173 | let iRect = image.extent 174 | var backBounds = CGRect(x: 0, y: 0, width: dSize.width, height: dSize.height) 175 | 176 | image = image.cropped(to: backBounds) 177 | 178 | let shiftX: CGFloat 179 | let shiftY: CGFloat 180 | 181 | shiftX = round((backBounds.size.width + iRect.origin.x - iRect.size.width) * 0.5) 182 | shiftY = round((backBounds.size.height + iRect.origin.y - iRect.size.height) * 0.5) 183 | 184 | // Read the center port of the image. 185 | backBounds = backBounds.offsetBy(dx: -shiftX, dy: -shiftY) 186 | 187 | // Blend the image over an opaque background image. 188 | // This is needed if the image is smaller than the view, or if it has transparent pixels. 189 | image = image.composited(over: self.opaqueBackground) 190 | 191 | let block = { 192 | if self.clearDestination { 193 | _ = try? ciContext.startTask(toClear: destination) 194 | } 195 | 196 | // Start a task that renders to the texture destination. 197 | // To prevent showing clamped content outside of the image's extent, we just render the 198 | // original extent from the image. 199 | _ = try? ciContext.startTask( 200 | toRender: image, 201 | from: iRect, 202 | to: destination, 203 | at: CGPoint(x: shiftX, y: shiftY) 204 | ) 205 | 206 | // Insert a command to present the drawable when the buffer has been scheduled for execution. 207 | commandBuffer.present(drawable) 208 | 209 | // Commit the command buffer so that the GPU executes the work that the Core Image Render Task issues. 210 | commandBuffer.commit() 211 | } 212 | 213 | if let queue = queue { 214 | queue.async { 215 | block() 216 | } 217 | } else { 218 | block() 219 | } 220 | } 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LockedCameraCapture Extension Demo & Explanation 2 | 3 | ![](./Doc/hero.jpg) 4 | 5 | Starting from iOS 18, we can integrate the `LockedCameraCapture` framework to our apps and let users launch 3rd-party camera apps directly on the Lock Screen by either Lock Screen Control or the Action button without unlocking the iPhone. 6 | 7 | Apple has provided documentation for this feature at the following link: [documentation](https://developer.apple.com/documentation/LockedCameraCapture/Creating-a-camera-experience-for-the-Lock-Screen). However, there is currently no official demo available, and there are some subtle details that developers should be aware of. 8 | 9 | Here we are. I created this demo while experimenting with the `LockedCameraCapture` framework and integrating it into my upcoming release of the [PhotonCam](https://apps.apple.com/us/app/photoncam-better-proraw-cam/id6470341051?l) app. It’s a ProRAW camera app that offers Pro-level control, extensive customization options like filters and frames, and a photo editing feature. 10 | 11 | Before reading the detailed explanation below, you can also build and run the demo app first. 12 | 13 | > Note that due to the issue of iOS 18 Beta, you may find that the Capture Extension in this demo may not launch from the lock screen. Try uninstalling the app and rebooting your device to solve this issue. 14 | 15 | The following sections will talk about some key implementation details and I highly suggest you reading the Apple [documentation](https://developer.apple.com/documentation/LockedCameraCapture/Creating-a-camera-experience-for-the-Lock-Screen) first. 16 | 17 | > You are welcome to subscribe my [Substack](https://juniperphoton.substack.com/p/lockedcameracapture-extension-demo) for free to view more articles. 18 | 19 | The content of this article: 20 | 21 | 1. The anatomy of your app 22 | 2. How to debug the Capture Extension 23 | 3. Prevent the extension from being killed 24 | 4. Code sharing between main app & Capture Extension 25 | 5. Scene Phase handling 26 | 6. Store temporary files in the Capture Extension 27 | 7. Lock device orientation 28 | 8. Custom symbol image for ControlWidget 29 | 9. Be aware of the bundle size 30 | 10. Additional Information 31 | 32 | ## The anatomy of your app 33 | 34 | The app that implements the `LockedCameraCapture` feature should at least contain 3 different parts: 35 | 36 | 1. Your main app that will launch when users click the app icon on the home screen. 37 | 2. The Widget Extension provides Control Widget, which was introduced in iOS 18 to let users launch your app via Control Center or Lock Screen. 38 | 3. The Capture Extension that conforms to the `LockedCameraCaptureExtension` protocol and provides the entry point for your extension app. 39 | 40 | Here is the illustration of the launching process: 41 | 42 | ![](./Doc/anatomy.jpg) 43 | 44 | The following sections talk about some key details that need to be aware of while implementing the Capture Extension. 45 | 46 | ## How to debug the Capture Extension 47 | 48 | For a regular iOS app, when launching from Xcode, you don't worry about how the debugger is attached. If your app crashes during the launch, Xcode will guide you to the code where the crash happens. 49 | 50 | In contrast, the automatic attaching process for the app extension is not enabled. If you launch your app from Xcode, the debugger will only be attached to the main app. Consequently, if your extension crashes during launch, it becomes challenging to identify the cause of the issue. 51 | 52 | Xcode offers a mechanism to defer the attachment of the debugger to a process. 53 | 54 | ![](./Doc/debugger.png) 55 | 56 | Access the Debug menu and locate the option “Attach to Process by PID or Name.” Subsequently, input the name of your Capture Extension. **By executing this action, even if the process you intend to debug is not currently running, Xcode will establish a connection with it upon its subsequent activation.** 57 | 58 | If your Capture Extension is currently running, you can simply locate the Process under the “Attach to Process” submenu to debug your code. 59 | 60 | ## Prevent the extension from being killed 61 | 62 | The Capture Extension, as its name suggests, is specifically designed for capturing purposes. Apple has implemented certain restrictions to ensure that the extension is solely intended for capturing functions, such as: 63 | 64 | 1. You should start your camera session while the extension is running. 65 | 2. Add `AVCaptureEventInteraction` to the view, which enables the hardware to detect pressing events. 66 | 67 | If your Capture Extension doesn’t adhere to the limitations mentioned above, it will be terminated after approximately 10 seconds. 68 | 69 | Please be aware that when the users click the Control Widget, iOS will determine whether to launch the main app or the Capture Extension based on the current conditions, like whether your device is locked or not. 70 | 71 | > For testing purposes, you should lock your iPhone manually, turn your face around, and launch the app via the Action button or Control Widget on the Lock Screen. 72 | 73 | ### Implement `AVCaptureEventInteraction` 74 | 75 | > UPDATE: On iOS 18.0, you can use [`onCameraCaptureEvent(isEnabled:primaryAction:secondaryAction:)`](https://developer.apple.com/documentation/SwiftUI/View/onCameraCaptureEvent(isEnabled:primaryAction:secondaryAction:)) in SwiftUI to provide such functionality. 76 | 77 | The `AVCaptureEventInteraction` is a subclass of `UIInteraction` introduced in iOS 17.2 as part of the UIKit framework. 78 | 79 | > `UIInteraction`: The protocol that an interaction implements to access the view that owns it. 80 | 81 | `UIView` has an instance method called `addInteraction(_:)` to add a specific `UIInteraction`. 82 | 83 | However, since Apple is transitioning to SwiftUI, and `LockedCameraCaptureUIScene` expects a SwiftUI view, it seems weird for Apple to offer such an API that can't be directly used in SwiftUI. 84 | 85 | You might want to retrieve the `UIView` from the current `UIWindow`’s root `UIViewController`. However, for the iOS extension, there’s no way to access the current `UIScene` because `UIApplication.shared` isn’t available in the extension. 86 | 87 | Therefore, we can add our custom `UIView` to the hierarchy of SwiftUI views, as shown below: 88 | 89 | ```swift 90 | @available(iOS 17.2, *) 91 | private struct CaptureInteractionView: UIViewRepresentable { 92 | var action: () -> Void 93 | 94 | func makeUIView(context: Context) -> some UIView { 95 | let uiView = UIView() 96 | let interaction = AVCaptureEventInteraction { event in 97 | if event.phase == .began { 98 | action() 99 | } 100 | } 101 | 102 | uiView.addInteraction(interaction) 103 | return uiView 104 | } 105 | 106 | func updateUIView(_ uiView: UIViewType, context: Context) { 107 | // ignored 108 | } 109 | } 110 | ``` 111 | 112 | We can also implement an extension method for the `View` class to handle interaction events. 113 | 114 | ```swift 115 | extension View { 116 | @ViewBuilder 117 | func onPressCapture(action: @escaping () -> Void) -> some View { 118 | if #available(iOS 17.2, *) { 119 | self.background { 120 | CaptureInteractionView(action: action) 121 | } 122 | } else { 123 | self 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | Then you can use the `onPressCapture(action:)` on your root view: 130 | 131 | ```swift 132 | YourView { 133 | // content 134 | }.onPressCapture { 135 | Task { 136 | await viewModel.capturePhoto() 137 | } 138 | } 139 | ``` 140 | 141 | ## Code sharing between main app & Capture Extension 142 | 143 | To share code or assets between your main app and the Capture Extension, you can mark your code files as belonging to the Targets. 144 | 145 | When you add a file to your Xcode project, you can specify which targets should include that file. The “Target Membership” section in the File Inspector (on the right pane of Xcode) presents a checklist of all the targets in your project. You can check or uncheck targets to control whether the file is compiled into the corresponding products. 146 | 147 | Note that compiling all the files from the main app to the Capture Extension will result in a larger compiled bundle size. Consequently, users will require more time and data to download your app from the App Store. 148 | 149 | Also, not all the code you write in the main app can be compiled for the Capture Extension. For instance, the `UIApplication.shared` API is unavailable for all types of extensions. Therefore, if you rely on `UIApplication.shared` to obtain `UIScene`, `UIWindow`, `UIApplication.shared.alternateIconName`, or other related instances, you should consider using those methods to handle this situation. 150 | 151 | 1. Avoid using `UIApplication.shared` and explore alternative methods to obtain the desired functionality. 152 | 2. Ensure that the code using `UIApplication.shared` is included in the main app target and provide a stub version of it that is included in the Capture Extension target. 153 | 3. Add a Swift custom flag to the build settings for your extension target and use the `#if` directive to conditionally compile code. 154 | 155 | ![](./Doc/custom_flags.png) 156 | 157 | > Note that your Capture Extension should prioritize providing a seamless capture experience for users. Additionally, you should prompt users to open the main app when they are accessing unrelated features. 158 | 159 | ## Scene Phase handling 160 | 161 | In the Capture Extension, it is safe to assume that the `ScenePhase` environment variable is always active. However, when retrieving its value from `EnvironmentValues`, its value will always be `.background`. Any logic relied on this value, for example, you might want to perform some tasks when the scene is in foreground, will be invalid. 162 | 163 | To address this issue, you don’t need to modify each component of your logic. Instead, you can simply introduce your own `ScenePhase` to the root view. However, if you’re not utilizing SwiftUI’s lifecycle, you’ll need to create your own version of `ScenePhase` and inject it into your code. 164 | 165 | ```swift 166 | struct LockedCameraCaptureView: View { 167 | var body: some View { 168 | ContentView() 169 | .environment(\.scenePhase, .active) 170 | } 171 | } 172 | ``` 173 | 174 | ## Data sharing between the main app and the Capture Extension. 175 | 176 | The challenging aspect of implementing the `LockedCameraCapture` extension is its distinction from other iOS extensions, such as the widget extension, and other extensions in general. Unlike these extensions, which can share `UserDefaults` and files through the `AppGroup` mechanism, the `LockedCameraCapture` extension lacks this capability due to the privacy concern. 177 | 178 | For Capture Extension, the only way to share your configurations is via the `CameraCaptureIntent.appContext` API or the `LockedCameraCaptureManager` to access the directories containing captured content. 179 | 180 | ### Share your configurations 181 | 182 | Consider the code below: 183 | 184 | ```swift 185 | struct SwitchCameraButton: View { 186 | @AppStorage("UseFrontCamera") 187 | private var useFrontCamera: Bool = false 188 | 189 | var body: some View { 190 | Button { 191 | useFrontCamera.toggle() 192 | } label: { 193 | // label 194 | } 195 | } 196 | } 197 | ``` 198 | 199 | You might have relied on SwiftUI’s `AppStorage` property wrapper and `UserDefaults` to store the configurations of your UI, such as the camera position, user-selected zooming scale, or the ProRAW switch. However, since the Capture Extension can’t access or modify the shared `UserDefaults`, the state wrapped with `AppStorage` won’t function as intended. 200 | 201 | If your app’s minimum target version is iOS 18, you can fully utilize the `CameraCaptureIntent.appContext` to store configurations that should be shared across the main app and the Capture Extension. However, there’s a size limitation of 4 KB for `appContext`. For better compatibility, you still need to store the configurations in both `UserDefaults` and `CameraCaptureIntent.appContext`. 202 | 203 | - When the value of your switch changes, it must be written to both `UserDefaults` and `CameraCaptureIntent.appContext` for the main app. However, it should avoid writing to `UserDefaults` in the Capture Extension. 204 | - You can't use the `AppStorage` property wrapper to let SwiftUI automatically update your view. You should declare the configurations in your view model, and when the values are updated, invoke `objectWillChange.send()` to trigger UI update. 205 | - The initial value of your configuration should be read in the order of `CameraCaptureIntent.appContext`, `UserDefaults`, and a fallback value. 206 | 207 | Here’s an example of code to read configurations from the `appContext` and `UserDefaults`. 208 | 209 | ```swift 210 | @MainActor 211 | func updateFromAppContext() async { 212 | #if !CAPTURE_EXTENSION 213 | // If it's in the main app, first read the value from the UserDefaults. 214 | self.cameraPosition = AppUserDefaultSettings.shared.cameraPosition 215 | #endif 216 | 217 | if #available(iOS 18, *) { 218 | do { 219 | // If `AppCaptureIntent.appContext` exists, then read from it. 220 | if let appContext = try await AppCaptureIntent.appContext { 221 | self.cameraPosition = appContext.cameraPosition 222 | } 223 | } catch { 224 | print("error on getting app context") 225 | } 226 | } 227 | } 228 | ``` 229 | 230 | ### Share files 231 | 232 | Technically, the main app and the Capture Extension can't share files since what `LockedCameraCaptureManager` has provided is a one-way method: You use `LockedCameraCaptureManager` in your main app to listen for the updates of session contents, and you can then invalidate the URLs after processing them properly. 233 | 234 | ```swift 235 | for await update in LockedCameraCaptureManager.shared.sessionContentUpdates { 236 | switch update { 237 | case .initial(let urls): 238 | // Process captured content from existing session content directories. 239 | break 240 | case .added(let url): 241 | // Process captured content from a new session content directory. 242 | break 243 | case .removed(let url): 244 | // Process captured content from a removed session content directory. 245 | break 246 | default: 247 | // An unknown sessionContentUpdate was received. 248 | break 249 | } 250 | } 251 | ``` 252 | 253 | > For some unknown reasons, no updates will be emited when I test this API in Xcode 16 Beta 6 with iOS 18 Beta 7. I have fired the issue to Apple via the Feedback app. 254 | 255 | For the files created in your main app, even those under the `AppGroup`, the Capture Extension doesn’t have access to them. This includes the SQLite files used by SwiftData or Core Data. If you’re using SwiftData or Core Data to store user data, you’ll need to provide an in-memory database and disable any features that require writing to the database. 256 | 257 | For my PhotonCam app, I store users’ customized filters, aspect ratios, and frames using Core Data. While in the Capture Extension, I’ll provide default presets of these features for users to use. 258 | 259 | ## Store temporary files in the Capture Extension 260 | 261 | You could have used those methods below to obtain the root directory and then append the path and names to create a file URL for storing the file. 262 | 263 | ```swift 264 | func getRootURL() -> URL? { 265 | try? FileManager.default.url( 266 | for: .documentDirectory, 267 | in: .userDomainMask, 268 | appropriateFor: nil, 269 | create: true 270 | ) 271 | 272 | // Or URL.documentsDirectory 273 | } 274 | 275 | func getSharedRootURL() -> URL? { 276 | try? FileManager.default.containerURL( 277 | forSecurityApplicationGroupIdentifier: "com.juniperphoton.appgroup" 278 | ) 279 | } 280 | ``` 281 | 282 | In the Capture Extension, the URL we got here is not accessible. Actually, the `LockedCameraCaptureSession` provided in the `LockedCameraCaptureUIScene` has a `sessionContentURL` property for us to use as the root URL. 283 | 284 | Here is a machanism to help you setup the correct rootURL in the entry point of your app, and read it throughout the rest of your app. 285 | 286 | You can declare a `AppStorageConfigProvider` that provides the rootURL and a `standard` shared instance for the main app to use. 287 | 288 | ```swift 289 | struct AppStorageConfigProvider { 290 | let rootURL: URL? 291 | } 292 | 293 | extension AppStorageConfigProvider { 294 | static let standard = AppStorageConfigProvider( 295 | rootURL: getStandardRootURL() 296 | ) 297 | } 298 | ``` 299 | 300 | To help creating an instance of `AppStorageConfigProvider` from `LockedCameraCaptureSession`, you can have another initializer that takes `LockedCameraCaptureSession`: 301 | 302 | ```swift 303 | @available(iOS 18, *) 304 | extension AppStorageConfigProvider { 305 | init(_ session: LockedCameraCaptureSession) { 306 | self.rootURL = session.sessionContentURL 307 | } 308 | } 309 | ``` 310 | 311 | In your main app's entry: 312 | 313 | ```swift 314 | @main 315 | struct LockedCameraCaptureExtensionDemoApp: App { 316 | var body: some Scene { 317 | WindowGroup { 318 | ContentView(configProvider: AppStorageConfigProvider.standard) 319 | } 320 | } 321 | } 322 | ``` 323 | 324 | In your Capture Extension: 325 | 326 | ```swift 327 | @main 328 | struct LockedExtension: LockedCameraCaptureExtension { 329 | var body: some LockedCameraCaptureExtensionScene { 330 | LockedCameraCaptureUIScene { session in 331 | LockedCameraCaptureView(session: session) 332 | } 333 | } 334 | } 335 | 336 | struct LockedCameraCaptureView: View { 337 | let session: LockedCameraCaptureSession 338 | 339 | var body: some View { 340 | ContentView(configProvider: AppStorageConfigProvider(session)) 341 | } 342 | } 343 | ``` 344 | 345 | Well, there are numerous other ways to inject the `AppStorageConfigProvider` into the rest of your application, such as using SwiftUI’s `EnvironmentObject`, `EnvironmentValues`, or other Dependencies Injection frameworks. Dependencies management is crucial when building a robust app, and it won’t be discussed further in this section. 346 | 347 | ## Lock device orientation 348 | 349 | For the main target, there is a settings in Xcode to let you select the supported device orientations. For most camera app, you typically handle the orientation by rotating some UI elements like buttons or labels, instead of rotating the whole UI. 350 | 351 | For the Capture Extension, there is no such UI for you to configure. However, you can add the `UISupportedInterfaceOrientations` to the `Info.plist` in the Capture Extension. 352 | 353 | ![orientation](https://github.com/user-attachments/assets/27afa359-4c05-433c-8d15-ff1132d5c231) 354 | 355 | ## Custom symbol image for ControlWidget 356 | 357 | According to the [documentation](https://developer.apple.com/documentation/swiftui/controlwidget), only some particular views can be correctly rendered in a ControlWidget. 358 | 359 | > Controls are defined using templates in order to ensure that they control will work at all sizes and in all system spaces in which they might be displayed. These templates define images (specifically, symbol images) and text using simple SwiftUI views like [`Label`](https://developer.apple.com/documentation/swiftui/label), [`Text`](https://developer.apple.com/documentation/swiftui/text), and [`Image`](https://developer.apple.com/documentation/swiftui/image); and tint colors using the [`tint(_:)`](https://developer.apple.com/documentation/swiftui/controlwidgettemplate/tint(_:)) modifier. 360 | 361 | Particularly, regarding to images, only symbol images can be rendered. 362 | 363 | If the current SFSymbol images are good enough for you, then you can use those images using Image(systemName:) or `Label(:systemImage:)` inside the ControlWidget. However if you want to use your custom images, simple rasterized images won’t work in this case. 364 | 365 | To work with your customized images, you have to make it a symbol. Thera are a few guides that help you create your own custom symbol image: 366 | 367 | https://developer.apple.com/documentation/uikit/uiimage/creating_custom_symbol_images_for_your_app 368 | 369 | https://developer.apple.com/wwdc21/10250 370 | 371 | ## Be aware of the bundle size 372 | 373 | Building and including all source codes and assets for all targets will increase the app bundle size. Here are some techniques that can help analyze and reduce the bundle size. 374 | 375 | https://juniperphoton.substack.com/p/reducing-bundle-size-my-approach?r=1ss9aj 376 | 377 | ## Additional Information 378 | 379 | ### CameraCaptureIntent implementation should be built against the app target 380 | 381 | The implementation of ``CameraCaptureIntent`` should always be built against the App and the extension targets; otherwise, it won't be discovered as expected. 382 | 383 | If you own your framework, it possible that you put this implementation in the framework and set the target membership to be included in the App and the extension targets, even if the source code is in the framework itself. 384 | 385 | > Note: it looks like there is a specific API [AppIntentsPackage](https://developer.apple.com/documentation/appintents/appintentspackage) that should solve this discovery issue. But it won't work. 386 | 387 | There is a post about this issue: 388 | [AppIntentsPackage protocol with SPM package not working](https://forums.developer.apple.com/forums/thread/732535). 389 | -------------------------------------------------------------------------------- /LockedCameraCaptureExtensionDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2206C5302CE31EF600744B0F /* MetalLib in Frameworks */ = {isa = PBXBuildFile; productRef = 2206C52F2CE31EF600744B0F /* MetalLib */; }; 11 | 2206C5322CE31EFC00744B0F /* MetalLib in Frameworks */ = {isa = PBXBuildFile; productRef = 2206C5312CE31EFC00744B0F /* MetalLib */; }; 12 | 22722BB92CE3258E0045DB78 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 22722BB82CE3258E0045DB78 /* Models */; }; 13 | 22722BBD2CE325980045DB78 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 22722BBC2CE325980045DB78 /* Models */; }; 14 | 22722BC32CE325FD0045DB78 /* MetalLib in Frameworks */ = {isa = PBXBuildFile; productRef = 22722BC22CE325FD0045DB78 /* MetalLib */; }; 15 | 22722BC62CE326050045DB78 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 22722BC52CE326050045DB78 /* Models */; }; 16 | 22722D542CE3314A0045DB78 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 22722D532CE3314A0045DB78 /* Models */; }; 17 | 22DE8DFA2C74B3CA00FC6EEA /* LockedExtension.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 22DE8DF12C74B3CA00FC6EEA /* LockedExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 18 | 22DE8E072C74B3E500FC6EEA /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22DE8E062C74B3E500FC6EEA /* WidgetKit.framework */; }; 19 | 22DE8E092C74B3E500FC6EEA /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22DE8E082C74B3E500FC6EEA /* SwiftUI.framework */; }; 20 | 22DE8E142C74B3E600FC6EEA /* LockedWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 22DE8E042C74B3E500FC6EEA /* LockedWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 21 | 22DE8E462C74CC7C00FC6EEA /* LockedCameraCapture.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22DE8E452C74CC7C00FC6EEA /* LockedCameraCapture.framework */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXContainerItemProxy section */ 25 | 22DE8DF82C74B3CA00FC6EEA /* PBXContainerItemProxy */ = { 26 | isa = PBXContainerItemProxy; 27 | containerPortal = 22DE8DD32C74B36800FC6EEA /* Project object */; 28 | proxyType = 1; 29 | remoteGlobalIDString = 22DE8DF02C74B3CA00FC6EEA; 30 | remoteInfo = LockedExtension; 31 | }; 32 | 22DE8E122C74B3E600FC6EEA /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = 22DE8DD32C74B36800FC6EEA /* Project object */; 35 | proxyType = 1; 36 | remoteGlobalIDString = 22DE8E032C74B3E500FC6EEA; 37 | remoteInfo = LockedWidgetExtension; 38 | }; 39 | /* End PBXContainerItemProxy section */ 40 | 41 | /* Begin PBXCopyFilesBuildPhase section */ 42 | 22722D4B2CE32FDA0045DB78 /* Embed Frameworks */ = { 43 | isa = PBXCopyFilesBuildPhase; 44 | buildActionMask = 2147483647; 45 | dstPath = ""; 46 | dstSubfolderSpec = 10; 47 | files = ( 48 | ); 49 | name = "Embed Frameworks"; 50 | runOnlyForDeploymentPostprocessing = 0; 51 | }; 52 | 22DE8DFF2C74B3CA00FC6EEA /* Embed ExtensionKit Extensions */ = { 53 | isa = PBXCopyFilesBuildPhase; 54 | buildActionMask = 2147483647; 55 | dstPath = "$(EXTENSIONS_FOLDER_PATH)"; 56 | dstSubfolderSpec = 16; 57 | files = ( 58 | 22DE8DFA2C74B3CA00FC6EEA /* LockedExtension.appex in Embed ExtensionKit Extensions */, 59 | ); 60 | name = "Embed ExtensionKit Extensions"; 61 | runOnlyForDeploymentPostprocessing = 0; 62 | }; 63 | 22DE8E192C74B3E600FC6EEA /* Embed Foundation Extensions */ = { 64 | isa = PBXCopyFilesBuildPhase; 65 | buildActionMask = 2147483647; 66 | dstPath = ""; 67 | dstSubfolderSpec = 13; 68 | files = ( 69 | 22DE8E142C74B3E600FC6EEA /* LockedWidgetExtension.appex in Embed Foundation Extensions */, 70 | ); 71 | name = "Embed Foundation Extensions"; 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | /* End PBXCopyFilesBuildPhase section */ 75 | 76 | /* Begin PBXFileReference section */ 77 | 2206C52E2CE31E9100744B0F /* MetalLib */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MetalLib; path = Packages/MetalLib; sourceTree = ""; }; 78 | 22722BB02CE324C80045DB78 /* Models */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Models; path = Packages/Models; sourceTree = ""; }; 79 | 22722CE42CE32F0F0045DB78 /* IntentsLib.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = IntentsLib.xcodeproj; sourceTree = ""; }; 80 | 22DE8DDB2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LockedCameraCaptureExtensionDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 81 | 22DE8DF12C74B3CA00FC6EEA /* LockedExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = LockedExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 82 | 22DE8E042C74B3E500FC6EEA /* LockedWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LockedWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 83 | 22DE8E062C74B3E500FC6EEA /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 84 | 22DE8E082C74B3E500FC6EEA /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 85 | 22DE8E452C74CC7C00FC6EEA /* LockedCameraCapture.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LockedCameraCapture.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/LockedCameraCapture.framework; sourceTree = DEVELOPER_DIR; }; 86 | /* End PBXFileReference section */ 87 | 88 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 89 | 22722D522CE331180045DB78 /* Exceptions for "LockedCameraCaptureExtensionDemo" folder in "LockedWidgetExtension" target */ = { 90 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 91 | membershipExceptions = ( 92 | Intent/AppCaptureIntent.swift, 93 | ); 94 | target = 22DE8E032C74B3E500FC6EEA /* LockedWidgetExtension */; 95 | }; 96 | 22DE8DFB2C74B3CA00FC6EEA /* Exceptions for "LockedExtension" folder in "LockedExtension" target */ = { 97 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 98 | membershipExceptions = ( 99 | Info.plist, 100 | ); 101 | target = 22DE8DF02C74B3CA00FC6EEA /* LockedExtension */; 102 | }; 103 | 22DE8E152C74B3E600FC6EEA /* Exceptions for "LockedWidget" folder in "LockedWidgetExtension" target */ = { 104 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 105 | membershipExceptions = ( 106 | Info.plist, 107 | ); 108 | target = 22DE8E032C74B3E500FC6EEA /* LockedWidgetExtension */; 109 | }; 110 | 22DE8E2E2C74B5FF00FC6EEA /* Exceptions for "LockedCameraCaptureExtensionDemo" folder in "LockedExtension" target */ = { 111 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 112 | membershipExceptions = ( 113 | Camera/AppStorageConfigProvider.swift, 114 | Camera/OpenMainAppAction.swift, 115 | Camera/View/CaptureInteractionView.swift, 116 | Camera/View/ContentView.swift, 117 | Camera/ViewModel/CamPreviewViewModel.swift, 118 | Camera/ViewModel/CaptureProcessor.swift, 119 | Camera/ViewModel/MainViewModel.swift, 120 | Intent/AppCaptureIntent.swift, 121 | Localizable.xcstrings, 122 | ); 123 | target = 22DE8DF02C74B3CA00FC6EEA /* LockedExtension */; 124 | }; 125 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ 126 | 127 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 128 | 22DE8DDD2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo */ = { 129 | isa = PBXFileSystemSynchronizedRootGroup; 130 | exceptions = ( 131 | 22DE8E2E2C74B5FF00FC6EEA /* Exceptions for "LockedCameraCaptureExtensionDemo" folder in "LockedExtension" target */, 132 | 22722D522CE331180045DB78 /* Exceptions for "LockedCameraCaptureExtensionDemo" folder in "LockedWidgetExtension" target */, 133 | ); 134 | path = LockedCameraCaptureExtensionDemo; 135 | sourceTree = ""; 136 | }; 137 | 22DE8DF22C74B3CA00FC6EEA /* LockedExtension */ = { 138 | isa = PBXFileSystemSynchronizedRootGroup; 139 | exceptions = ( 140 | 22DE8DFB2C74B3CA00FC6EEA /* Exceptions for "LockedExtension" folder in "LockedExtension" target */, 141 | ); 142 | path = LockedExtension; 143 | sourceTree = ""; 144 | }; 145 | 22DE8E0A2C74B3E500FC6EEA /* LockedWidget */ = { 146 | isa = PBXFileSystemSynchronizedRootGroup; 147 | exceptions = ( 148 | 22DE8E152C74B3E600FC6EEA /* Exceptions for "LockedWidget" folder in "LockedWidgetExtension" target */, 149 | ); 150 | path = LockedWidget; 151 | sourceTree = ""; 152 | }; 153 | /* End PBXFileSystemSynchronizedRootGroup section */ 154 | 155 | /* Begin PBXFrameworksBuildPhase section */ 156 | 22DE8DD82C74B36800FC6EEA /* Frameworks */ = { 157 | isa = PBXFrameworksBuildPhase; 158 | buildActionMask = 2147483647; 159 | files = ( 160 | 22722BC62CE326050045DB78 /* Models in Frameworks */, 161 | 22722BC32CE325FD0045DB78 /* MetalLib in Frameworks */, 162 | 22722BB92CE3258E0045DB78 /* Models in Frameworks */, 163 | 2206C5302CE31EF600744B0F /* MetalLib in Frameworks */, 164 | ); 165 | runOnlyForDeploymentPostprocessing = 0; 166 | }; 167 | 22DE8DEE2C74B3CA00FC6EEA /* Frameworks */ = { 168 | isa = PBXFrameworksBuildPhase; 169 | buildActionMask = 2147483647; 170 | files = ( 171 | 22722BBD2CE325980045DB78 /* Models in Frameworks */, 172 | 2206C5322CE31EFC00744B0F /* MetalLib in Frameworks */, 173 | ); 174 | runOnlyForDeploymentPostprocessing = 0; 175 | }; 176 | 22DE8E012C74B3E500FC6EEA /* Frameworks */ = { 177 | isa = PBXFrameworksBuildPhase; 178 | buildActionMask = 2147483647; 179 | files = ( 180 | 22DE8E092C74B3E500FC6EEA /* SwiftUI.framework in Frameworks */, 181 | 22DE8E072C74B3E500FC6EEA /* WidgetKit.framework in Frameworks */, 182 | 22722D542CE3314A0045DB78 /* Models in Frameworks */, 183 | 22DE8E462C74CC7C00FC6EEA /* LockedCameraCapture.framework in Frameworks */, 184 | ); 185 | runOnlyForDeploymentPostprocessing = 0; 186 | }; 187 | /* End PBXFrameworksBuildPhase section */ 188 | 189 | /* Begin PBXGroup section */ 190 | 22722CE62CE32F0F0045DB78 /* Products */ = { 191 | isa = PBXGroup; 192 | children = ( 193 | ); 194 | name = Products; 195 | sourceTree = ""; 196 | }; 197 | 22DE8DD22C74B36800FC6EEA = { 198 | isa = PBXGroup; 199 | children = ( 200 | 22722BB02CE324C80045DB78 /* Models */, 201 | 2206C52E2CE31E9100744B0F /* MetalLib */, 202 | 22DE8DDD2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo */, 203 | 22DE8DF22C74B3CA00FC6EEA /* LockedExtension */, 204 | 22DE8E0A2C74B3E500FC6EEA /* LockedWidget */, 205 | 22DE8E052C74B3E500FC6EEA /* Frameworks */, 206 | 22DE8DDC2C74B36800FC6EEA /* Products */, 207 | ); 208 | sourceTree = ""; 209 | }; 210 | 22DE8DDC2C74B36800FC6EEA /* Products */ = { 211 | isa = PBXGroup; 212 | children = ( 213 | 22DE8DDB2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo.app */, 214 | 22DE8DF12C74B3CA00FC6EEA /* LockedExtension.appex */, 215 | 22DE8E042C74B3E500FC6EEA /* LockedWidgetExtension.appex */, 216 | ); 217 | name = Products; 218 | sourceTree = ""; 219 | }; 220 | 22DE8E052C74B3E500FC6EEA /* Frameworks */ = { 221 | isa = PBXGroup; 222 | children = ( 223 | 22DE8E452C74CC7C00FC6EEA /* LockedCameraCapture.framework */, 224 | 22DE8E062C74B3E500FC6EEA /* WidgetKit.framework */, 225 | 22DE8E082C74B3E500FC6EEA /* SwiftUI.framework */, 226 | ); 227 | name = Frameworks; 228 | sourceTree = ""; 229 | }; 230 | /* End PBXGroup section */ 231 | 232 | /* Begin PBXNativeTarget section */ 233 | 22DE8DDA2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo */ = { 234 | isa = PBXNativeTarget; 235 | buildConfigurationList = 22DE8DEA2C74B36900FC6EEA /* Build configuration list for PBXNativeTarget "LockedCameraCaptureExtensionDemo" */; 236 | buildPhases = ( 237 | 22DE8DD72C74B36800FC6EEA /* Sources */, 238 | 22DE8DD82C74B36800FC6EEA /* Frameworks */, 239 | 22DE8DD92C74B36800FC6EEA /* Resources */, 240 | 22DE8DFF2C74B3CA00FC6EEA /* Embed ExtensionKit Extensions */, 241 | 22DE8E192C74B3E600FC6EEA /* Embed Foundation Extensions */, 242 | 22722D4B2CE32FDA0045DB78 /* Embed Frameworks */, 243 | ); 244 | buildRules = ( 245 | ); 246 | dependencies = ( 247 | 22DE8DF92C74B3CA00FC6EEA /* PBXTargetDependency */, 248 | 22DE8E132C74B3E600FC6EEA /* PBXTargetDependency */, 249 | ); 250 | fileSystemSynchronizedGroups = ( 251 | 22DE8DDD2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo */, 252 | ); 253 | name = LockedCameraCaptureExtensionDemo; 254 | packageProductDependencies = ( 255 | 2206C52F2CE31EF600744B0F /* MetalLib */, 256 | 22722BB82CE3258E0045DB78 /* Models */, 257 | 22722BC22CE325FD0045DB78 /* MetalLib */, 258 | 22722BC52CE326050045DB78 /* Models */, 259 | ); 260 | productName = LockedCameraCaptureExtensionDemo; 261 | productReference = 22DE8DDB2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo.app */; 262 | productType = "com.apple.product-type.application"; 263 | }; 264 | 22DE8DF02C74B3CA00FC6EEA /* LockedExtension */ = { 265 | isa = PBXNativeTarget; 266 | buildConfigurationList = 22DE8DFC2C74B3CA00FC6EEA /* Build configuration list for PBXNativeTarget "LockedExtension" */; 267 | buildPhases = ( 268 | 22DE8DED2C74B3CA00FC6EEA /* Sources */, 269 | 22DE8DEE2C74B3CA00FC6EEA /* Frameworks */, 270 | 22DE8DEF2C74B3CA00FC6EEA /* Resources */, 271 | ); 272 | buildRules = ( 273 | ); 274 | dependencies = ( 275 | ); 276 | fileSystemSynchronizedGroups = ( 277 | 22DE8DF22C74B3CA00FC6EEA /* LockedExtension */, 278 | ); 279 | name = LockedExtension; 280 | packageProductDependencies = ( 281 | 2206C5312CE31EFC00744B0F /* MetalLib */, 282 | 22722BBC2CE325980045DB78 /* Models */, 283 | ); 284 | productName = LockedExtension; 285 | productReference = 22DE8DF12C74B3CA00FC6EEA /* LockedExtension.appex */; 286 | productType = "com.apple.product-type.extensionkit-extension"; 287 | }; 288 | 22DE8E032C74B3E500FC6EEA /* LockedWidgetExtension */ = { 289 | isa = PBXNativeTarget; 290 | buildConfigurationList = 22DE8E162C74B3E600FC6EEA /* Build configuration list for PBXNativeTarget "LockedWidgetExtension" */; 291 | buildPhases = ( 292 | 22DE8E002C74B3E500FC6EEA /* Sources */, 293 | 22DE8E012C74B3E500FC6EEA /* Frameworks */, 294 | 22DE8E022C74B3E500FC6EEA /* Resources */, 295 | ); 296 | buildRules = ( 297 | ); 298 | dependencies = ( 299 | ); 300 | fileSystemSynchronizedGroups = ( 301 | 22DE8E0A2C74B3E500FC6EEA /* LockedWidget */, 302 | ); 303 | name = LockedWidgetExtension; 304 | packageProductDependencies = ( 305 | 22722D532CE3314A0045DB78 /* Models */, 306 | ); 307 | productName = LockedWidgetExtension; 308 | productReference = 22DE8E042C74B3E500FC6EEA /* LockedWidgetExtension.appex */; 309 | productType = "com.apple.product-type.app-extension"; 310 | }; 311 | /* End PBXNativeTarget section */ 312 | 313 | /* Begin PBXProject section */ 314 | 22DE8DD32C74B36800FC6EEA /* Project object */ = { 315 | isa = PBXProject; 316 | attributes = { 317 | BuildIndependentTargetsInParallel = 1; 318 | LastSwiftUpdateCheck = 1600; 319 | LastUpgradeCheck = 1600; 320 | TargetAttributes = { 321 | 22DE8DDA2C74B36800FC6EEA = { 322 | CreatedOnToolsVersion = 16.0; 323 | }; 324 | 22DE8DF02C74B3CA00FC6EEA = { 325 | CreatedOnToolsVersion = 16.0; 326 | }; 327 | 22DE8E032C74B3E500FC6EEA = { 328 | CreatedOnToolsVersion = 16.0; 329 | }; 330 | }; 331 | }; 332 | buildConfigurationList = 22DE8DD62C74B36800FC6EEA /* Build configuration list for PBXProject "LockedCameraCaptureExtensionDemo" */; 333 | developmentRegion = en; 334 | hasScannedForEncodings = 0; 335 | knownRegions = ( 336 | en, 337 | Base, 338 | "zh-Hans", 339 | "zh-Hant", 340 | ); 341 | mainGroup = 22DE8DD22C74B36800FC6EEA; 342 | minimizedProjectReferenceProxies = 1; 343 | packageReferences = ( 344 | 22722BC12CE325FD0045DB78 /* XCLocalSwiftPackageReference "Packages/MetalLib" */, 345 | 22722BC42CE326050045DB78 /* XCLocalSwiftPackageReference "Packages/Models" */, 346 | ); 347 | preferredProjectObjectVersion = 77; 348 | productRefGroup = 22DE8DDC2C74B36800FC6EEA /* Products */; 349 | projectDirPath = ""; 350 | projectReferences = ( 351 | { 352 | ProductGroup = 22722CE62CE32F0F0045DB78 /* Products */; 353 | ProjectRef = 22722CE42CE32F0F0045DB78 /* IntentsLib.xcodeproj */; 354 | }, 355 | ); 356 | projectRoot = ""; 357 | targets = ( 358 | 22DE8DDA2C74B36800FC6EEA /* LockedCameraCaptureExtensionDemo */, 359 | 22DE8DF02C74B3CA00FC6EEA /* LockedExtension */, 360 | 22DE8E032C74B3E500FC6EEA /* LockedWidgetExtension */, 361 | ); 362 | }; 363 | /* End PBXProject section */ 364 | 365 | /* Begin PBXResourcesBuildPhase section */ 366 | 22DE8DD92C74B36800FC6EEA /* Resources */ = { 367 | isa = PBXResourcesBuildPhase; 368 | buildActionMask = 2147483647; 369 | files = ( 370 | ); 371 | runOnlyForDeploymentPostprocessing = 0; 372 | }; 373 | 22DE8DEF2C74B3CA00FC6EEA /* Resources */ = { 374 | isa = PBXResourcesBuildPhase; 375 | buildActionMask = 2147483647; 376 | files = ( 377 | ); 378 | runOnlyForDeploymentPostprocessing = 0; 379 | }; 380 | 22DE8E022C74B3E500FC6EEA /* Resources */ = { 381 | isa = PBXResourcesBuildPhase; 382 | buildActionMask = 2147483647; 383 | files = ( 384 | ); 385 | runOnlyForDeploymentPostprocessing = 0; 386 | }; 387 | /* End PBXResourcesBuildPhase section */ 388 | 389 | /* Begin PBXSourcesBuildPhase section */ 390 | 22DE8DD72C74B36800FC6EEA /* Sources */ = { 391 | isa = PBXSourcesBuildPhase; 392 | buildActionMask = 2147483647; 393 | files = ( 394 | ); 395 | runOnlyForDeploymentPostprocessing = 0; 396 | }; 397 | 22DE8DED2C74B3CA00FC6EEA /* Sources */ = { 398 | isa = PBXSourcesBuildPhase; 399 | buildActionMask = 2147483647; 400 | files = ( 401 | ); 402 | runOnlyForDeploymentPostprocessing = 0; 403 | }; 404 | 22DE8E002C74B3E500FC6EEA /* Sources */ = { 405 | isa = PBXSourcesBuildPhase; 406 | buildActionMask = 2147483647; 407 | files = ( 408 | ); 409 | runOnlyForDeploymentPostprocessing = 0; 410 | }; 411 | /* End PBXSourcesBuildPhase section */ 412 | 413 | /* Begin PBXTargetDependency section */ 414 | 22DE8DF92C74B3CA00FC6EEA /* PBXTargetDependency */ = { 415 | isa = PBXTargetDependency; 416 | target = 22DE8DF02C74B3CA00FC6EEA /* LockedExtension */; 417 | targetProxy = 22DE8DF82C74B3CA00FC6EEA /* PBXContainerItemProxy */; 418 | }; 419 | 22DE8E132C74B3E600FC6EEA /* PBXTargetDependency */ = { 420 | isa = PBXTargetDependency; 421 | target = 22DE8E032C74B3E500FC6EEA /* LockedWidgetExtension */; 422 | targetProxy = 22DE8E122C74B3E600FC6EEA /* PBXContainerItemProxy */; 423 | }; 424 | /* End PBXTargetDependency section */ 425 | 426 | /* Begin XCBuildConfiguration section */ 427 | 22DE8DE82C74B36900FC6EEA /* Debug */ = { 428 | isa = XCBuildConfiguration; 429 | buildSettings = { 430 | ALWAYS_SEARCH_USER_PATHS = NO; 431 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 432 | CLANG_ANALYZER_NONNULL = YES; 433 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 434 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 435 | CLANG_ENABLE_MODULES = YES; 436 | CLANG_ENABLE_OBJC_ARC = YES; 437 | CLANG_ENABLE_OBJC_WEAK = YES; 438 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 439 | CLANG_WARN_BOOL_CONVERSION = YES; 440 | CLANG_WARN_COMMA = YES; 441 | CLANG_WARN_CONSTANT_CONVERSION = YES; 442 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 443 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 444 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 445 | CLANG_WARN_EMPTY_BODY = YES; 446 | CLANG_WARN_ENUM_CONVERSION = YES; 447 | CLANG_WARN_INFINITE_RECURSION = YES; 448 | CLANG_WARN_INT_CONVERSION = YES; 449 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 450 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 451 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 452 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 453 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 454 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 455 | CLANG_WARN_STRICT_PROTOTYPES = YES; 456 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 457 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 458 | CLANG_WARN_UNREACHABLE_CODE = YES; 459 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 460 | COPY_PHASE_STRIP = NO; 461 | DEBUG_INFORMATION_FORMAT = dwarf; 462 | ENABLE_STRICT_OBJC_MSGSEND = YES; 463 | ENABLE_TESTABILITY = YES; 464 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 465 | GCC_C_LANGUAGE_STANDARD = gnu17; 466 | GCC_DYNAMIC_NO_PIC = NO; 467 | GCC_NO_COMMON_BLOCKS = YES; 468 | GCC_OPTIMIZATION_LEVEL = 0; 469 | GCC_PREPROCESSOR_DEFINITIONS = ( 470 | "DEBUG=1", 471 | "$(inherited)", 472 | ); 473 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 474 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 475 | GCC_WARN_UNDECLARED_SELECTOR = YES; 476 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 477 | GCC_WARN_UNUSED_FUNCTION = YES; 478 | GCC_WARN_UNUSED_VARIABLE = YES; 479 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 480 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 481 | MTL_FAST_MATH = YES; 482 | ONLY_ACTIVE_ARCH = YES; 483 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 484 | SWIFT_EMIT_LOC_STRINGS = YES; 485 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 486 | }; 487 | name = Debug; 488 | }; 489 | 22DE8DE92C74B36900FC6EEA /* Release */ = { 490 | isa = XCBuildConfiguration; 491 | buildSettings = { 492 | ALWAYS_SEARCH_USER_PATHS = NO; 493 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 494 | CLANG_ANALYZER_NONNULL = YES; 495 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 496 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 497 | CLANG_ENABLE_MODULES = YES; 498 | CLANG_ENABLE_OBJC_ARC = YES; 499 | CLANG_ENABLE_OBJC_WEAK = YES; 500 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 501 | CLANG_WARN_BOOL_CONVERSION = YES; 502 | CLANG_WARN_COMMA = YES; 503 | CLANG_WARN_CONSTANT_CONVERSION = YES; 504 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 505 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 506 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 507 | CLANG_WARN_EMPTY_BODY = YES; 508 | CLANG_WARN_ENUM_CONVERSION = YES; 509 | CLANG_WARN_INFINITE_RECURSION = YES; 510 | CLANG_WARN_INT_CONVERSION = YES; 511 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 512 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 513 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 514 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 515 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 516 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 517 | CLANG_WARN_STRICT_PROTOTYPES = YES; 518 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 519 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 520 | CLANG_WARN_UNREACHABLE_CODE = YES; 521 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 522 | COPY_PHASE_STRIP = NO; 523 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 524 | ENABLE_NS_ASSERTIONS = NO; 525 | ENABLE_STRICT_OBJC_MSGSEND = YES; 526 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 527 | GCC_C_LANGUAGE_STANDARD = gnu17; 528 | GCC_NO_COMMON_BLOCKS = YES; 529 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 530 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 531 | GCC_WARN_UNDECLARED_SELECTOR = YES; 532 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 533 | GCC_WARN_UNUSED_FUNCTION = YES; 534 | GCC_WARN_UNUSED_VARIABLE = YES; 535 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 536 | MTL_ENABLE_DEBUG_INFO = NO; 537 | MTL_FAST_MATH = YES; 538 | SWIFT_COMPILATION_MODE = wholemodule; 539 | SWIFT_EMIT_LOC_STRINGS = YES; 540 | }; 541 | name = Release; 542 | }; 543 | 22DE8DEB2C74B36900FC6EEA /* Debug */ = { 544 | isa = XCBuildConfiguration; 545 | buildSettings = { 546 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 547 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 548 | CODE_SIGN_ENTITLEMENTS = LockedCameraCaptureExtensionDemo/LockedCameraCaptureExtensionDemo.entitlements; 549 | CODE_SIGN_STYLE = Automatic; 550 | CURRENT_PROJECT_VERSION = 1; 551 | DEVELOPMENT_ASSET_PATHS = "\"LockedCameraCaptureExtensionDemo/Preview Content\""; 552 | DEVELOPMENT_TEAM = 7GB9CHCB87; 553 | ENABLE_HARDENED_RUNTIME = YES; 554 | ENABLE_PREVIEWS = YES; 555 | GENERATE_INFOPLIST_FILE = YES; 556 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography"; 557 | INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera permission to access and take photos."; 558 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app requires photo library permission to add photos to your library."; 559 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 560 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 561 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 562 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 563 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 564 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 565 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 566 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 567 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 568 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 569 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 570 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 571 | MACOSX_DEPLOYMENT_TARGET = 14.6; 572 | MARKETING_VERSION = 1.0; 573 | PRODUCT_BUNDLE_IDENTIFIER = com.juniperphoton.LockedCameraCaptureExtensionDemo; 574 | PRODUCT_NAME = "$(TARGET_NAME)"; 575 | SDKROOT = auto; 576 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 577 | SUPPORTS_MACCATALYST = NO; 578 | SWIFT_EMIT_LOC_STRINGS = YES; 579 | SWIFT_VERSION = 5.0; 580 | TARGETED_DEVICE_FAMILY = "1,2"; 581 | XROS_DEPLOYMENT_TARGET = 2.0; 582 | }; 583 | name = Debug; 584 | }; 585 | 22DE8DEC2C74B36900FC6EEA /* Release */ = { 586 | isa = XCBuildConfiguration; 587 | buildSettings = { 588 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 589 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 590 | CODE_SIGN_ENTITLEMENTS = LockedCameraCaptureExtensionDemo/LockedCameraCaptureExtensionDemo.entitlements; 591 | CODE_SIGN_STYLE = Automatic; 592 | CURRENT_PROJECT_VERSION = 1; 593 | DEVELOPMENT_ASSET_PATHS = "\"LockedCameraCaptureExtensionDemo/Preview Content\""; 594 | DEVELOPMENT_TEAM = 7GB9CHCB87; 595 | ENABLE_HARDENED_RUNTIME = YES; 596 | ENABLE_PREVIEWS = YES; 597 | GENERATE_INFOPLIST_FILE = YES; 598 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography"; 599 | INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera permission to access and take photos."; 600 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app requires photo library permission to add photos to your library."; 601 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 602 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 603 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 604 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 605 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 606 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 607 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 608 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 609 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 610 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 611 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 612 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 613 | MACOSX_DEPLOYMENT_TARGET = 14.6; 614 | MARKETING_VERSION = 1.0; 615 | PRODUCT_BUNDLE_IDENTIFIER = com.juniperphoton.LockedCameraCaptureExtensionDemo; 616 | PRODUCT_NAME = "$(TARGET_NAME)"; 617 | SDKROOT = auto; 618 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 619 | SUPPORTS_MACCATALYST = NO; 620 | SWIFT_EMIT_LOC_STRINGS = YES; 621 | SWIFT_VERSION = 5.0; 622 | TARGETED_DEVICE_FAMILY = "1,2"; 623 | XROS_DEPLOYMENT_TARGET = 2.0; 624 | }; 625 | name = Release; 626 | }; 627 | 22DE8DFD2C74B3CA00FC6EEA /* Debug */ = { 628 | isa = XCBuildConfiguration; 629 | buildSettings = { 630 | CODE_SIGN_STYLE = Automatic; 631 | CURRENT_PROJECT_VERSION = 1; 632 | DEVELOPMENT_TEAM = 7GB9CHCB87; 633 | GENERATE_INFOPLIST_FILE = YES; 634 | INFOPLIST_FILE = LockedExtension/Info.plist; 635 | INFOPLIST_KEY_CFBundleDisplayName = LockedExtension; 636 | INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera permission to access and take photos."; 637 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 638 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app requires photo library permission to add photos to your library."; 639 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 640 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; 641 | LD_RUNPATH_SEARCH_PATHS = ( 642 | "$(inherited)", 643 | "@executable_path/Frameworks", 644 | "@executable_path/../../Frameworks", 645 | ); 646 | MARKETING_VERSION = 1.0; 647 | PRODUCT_BUNDLE_IDENTIFIER = com.juniperphoton.LockedCameraCaptureExtensionDemo.LockedExtension; 648 | PRODUCT_NAME = "$(TARGET_NAME)"; 649 | SDKROOT = iphoneos; 650 | SKIP_INSTALL = YES; 651 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 652 | SUPPORTS_MACCATALYST = NO; 653 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 654 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 655 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited) CAPTURE_EXTENSION"; 656 | SWIFT_EMIT_LOC_STRINGS = YES; 657 | SWIFT_VERSION = 5.0; 658 | TARGETED_DEVICE_FAMILY = "1,2"; 659 | }; 660 | name = Debug; 661 | }; 662 | 22DE8DFE2C74B3CA00FC6EEA /* Release */ = { 663 | isa = XCBuildConfiguration; 664 | buildSettings = { 665 | CODE_SIGN_STYLE = Automatic; 666 | CURRENT_PROJECT_VERSION = 1; 667 | DEVELOPMENT_TEAM = 7GB9CHCB87; 668 | GENERATE_INFOPLIST_FILE = YES; 669 | INFOPLIST_FILE = LockedExtension/Info.plist; 670 | INFOPLIST_KEY_CFBundleDisplayName = LockedExtension; 671 | INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera permission to access and take photos."; 672 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 673 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app requires photo library permission to add photos to your library."; 674 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 675 | IPHONEOS_DEPLOYMENT_TARGET = 18.0; 676 | LD_RUNPATH_SEARCH_PATHS = ( 677 | "$(inherited)", 678 | "@executable_path/Frameworks", 679 | "@executable_path/../../Frameworks", 680 | ); 681 | MARKETING_VERSION = 1.0; 682 | PRODUCT_BUNDLE_IDENTIFIER = com.juniperphoton.LockedCameraCaptureExtensionDemo.LockedExtension; 683 | PRODUCT_NAME = "$(TARGET_NAME)"; 684 | SDKROOT = iphoneos; 685 | SKIP_INSTALL = YES; 686 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 687 | SUPPORTS_MACCATALYST = NO; 688 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 689 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 690 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = CAPTURE_EXTENSION; 691 | SWIFT_EMIT_LOC_STRINGS = YES; 692 | SWIFT_VERSION = 5.0; 693 | TARGETED_DEVICE_FAMILY = "1,2"; 694 | VALIDATE_PRODUCT = YES; 695 | }; 696 | name = Release; 697 | }; 698 | 22DE8E172C74B3E600FC6EEA /* Debug */ = { 699 | isa = XCBuildConfiguration; 700 | buildSettings = { 701 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 702 | ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; 703 | CODE_SIGN_STYLE = Automatic; 704 | CURRENT_PROJECT_VERSION = 1; 705 | DEVELOPMENT_TEAM = 7GB9CHCB87; 706 | GENERATE_INFOPLIST_FILE = YES; 707 | INFOPLIST_FILE = LockedWidget/Info.plist; 708 | INFOPLIST_KEY_CFBundleDisplayName = LockedWidget; 709 | INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera permission to access and take photos."; 710 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 711 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app requires photo library permission to add photos to your library."; 712 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 713 | LD_RUNPATH_SEARCH_PATHS = ( 714 | "$(inherited)", 715 | "@executable_path/Frameworks", 716 | "@executable_path/../../Frameworks", 717 | ); 718 | MARKETING_VERSION = 1.0; 719 | OTHER_SWIFT_FLAGS = ""; 720 | PRODUCT_BUNDLE_IDENTIFIER = com.juniperphoton.LockedCameraCaptureExtensionDemo.LockedWidget; 721 | PRODUCT_NAME = "$(TARGET_NAME)"; 722 | SDKROOT = iphoneos; 723 | SKIP_INSTALL = YES; 724 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 725 | SUPPORTS_MACCATALYST = NO; 726 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 727 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 728 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 729 | SWIFT_EMIT_LOC_STRINGS = YES; 730 | SWIFT_VERSION = 5.0; 731 | TARGETED_DEVICE_FAMILY = "1,2"; 732 | }; 733 | name = Debug; 734 | }; 735 | 22DE8E182C74B3E600FC6EEA /* Release */ = { 736 | isa = XCBuildConfiguration; 737 | buildSettings = { 738 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 739 | ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; 740 | CODE_SIGN_STYLE = Automatic; 741 | CURRENT_PROJECT_VERSION = 1; 742 | DEVELOPMENT_TEAM = 7GB9CHCB87; 743 | GENERATE_INFOPLIST_FILE = YES; 744 | INFOPLIST_FILE = LockedWidget/Info.plist; 745 | INFOPLIST_KEY_CFBundleDisplayName = LockedWidget; 746 | INFOPLIST_KEY_NSCameraUsageDescription = "This app requires camera permission to access and take photos."; 747 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 748 | INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app requires photo library permission to add photos to your library."; 749 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 750 | LD_RUNPATH_SEARCH_PATHS = ( 751 | "$(inherited)", 752 | "@executable_path/Frameworks", 753 | "@executable_path/../../Frameworks", 754 | ); 755 | MARKETING_VERSION = 1.0; 756 | OTHER_SWIFT_FLAGS = ""; 757 | PRODUCT_BUNDLE_IDENTIFIER = com.juniperphoton.LockedCameraCaptureExtensionDemo.LockedWidget; 758 | PRODUCT_NAME = "$(TARGET_NAME)"; 759 | SDKROOT = iphoneos; 760 | SKIP_INSTALL = YES; 761 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 762 | SUPPORTS_MACCATALYST = NO; 763 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 764 | SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; 765 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; 766 | SWIFT_EMIT_LOC_STRINGS = YES; 767 | SWIFT_VERSION = 5.0; 768 | TARGETED_DEVICE_FAMILY = "1,2"; 769 | VALIDATE_PRODUCT = YES; 770 | }; 771 | name = Release; 772 | }; 773 | /* End XCBuildConfiguration section */ 774 | 775 | /* Begin XCConfigurationList section */ 776 | 22DE8DD62C74B36800FC6EEA /* Build configuration list for PBXProject "LockedCameraCaptureExtensionDemo" */ = { 777 | isa = XCConfigurationList; 778 | buildConfigurations = ( 779 | 22DE8DE82C74B36900FC6EEA /* Debug */, 780 | 22DE8DE92C74B36900FC6EEA /* Release */, 781 | ); 782 | defaultConfigurationIsVisible = 0; 783 | defaultConfigurationName = Release; 784 | }; 785 | 22DE8DEA2C74B36900FC6EEA /* Build configuration list for PBXNativeTarget "LockedCameraCaptureExtensionDemo" */ = { 786 | isa = XCConfigurationList; 787 | buildConfigurations = ( 788 | 22DE8DEB2C74B36900FC6EEA /* Debug */, 789 | 22DE8DEC2C74B36900FC6EEA /* Release */, 790 | ); 791 | defaultConfigurationIsVisible = 0; 792 | defaultConfigurationName = Release; 793 | }; 794 | 22DE8DFC2C74B3CA00FC6EEA /* Build configuration list for PBXNativeTarget "LockedExtension" */ = { 795 | isa = XCConfigurationList; 796 | buildConfigurations = ( 797 | 22DE8DFD2C74B3CA00FC6EEA /* Debug */, 798 | 22DE8DFE2C74B3CA00FC6EEA /* Release */, 799 | ); 800 | defaultConfigurationIsVisible = 0; 801 | defaultConfigurationName = Release; 802 | }; 803 | 22DE8E162C74B3E600FC6EEA /* Build configuration list for PBXNativeTarget "LockedWidgetExtension" */ = { 804 | isa = XCConfigurationList; 805 | buildConfigurations = ( 806 | 22DE8E172C74B3E600FC6EEA /* Debug */, 807 | 22DE8E182C74B3E600FC6EEA /* Release */, 808 | ); 809 | defaultConfigurationIsVisible = 0; 810 | defaultConfigurationName = Release; 811 | }; 812 | /* End XCConfigurationList section */ 813 | 814 | /* Begin XCLocalSwiftPackageReference section */ 815 | 22722BC12CE325FD0045DB78 /* XCLocalSwiftPackageReference "Packages/MetalLib" */ = { 816 | isa = XCLocalSwiftPackageReference; 817 | relativePath = Packages/MetalLib; 818 | }; 819 | 22722BC42CE326050045DB78 /* XCLocalSwiftPackageReference "Packages/Models" */ = { 820 | isa = XCLocalSwiftPackageReference; 821 | relativePath = Packages/Models; 822 | }; 823 | /* End XCLocalSwiftPackageReference section */ 824 | 825 | /* Begin XCSwiftPackageProductDependency section */ 826 | 2206C52F2CE31EF600744B0F /* MetalLib */ = { 827 | isa = XCSwiftPackageProductDependency; 828 | productName = MetalLib; 829 | }; 830 | 2206C5312CE31EFC00744B0F /* MetalLib */ = { 831 | isa = XCSwiftPackageProductDependency; 832 | productName = MetalLib; 833 | }; 834 | 22722BB82CE3258E0045DB78 /* Models */ = { 835 | isa = XCSwiftPackageProductDependency; 836 | productName = Models; 837 | }; 838 | 22722BBC2CE325980045DB78 /* Models */ = { 839 | isa = XCSwiftPackageProductDependency; 840 | productName = Models; 841 | }; 842 | 22722BC22CE325FD0045DB78 /* MetalLib */ = { 843 | isa = XCSwiftPackageProductDependency; 844 | productName = MetalLib; 845 | }; 846 | 22722BC52CE326050045DB78 /* Models */ = { 847 | isa = XCSwiftPackageProductDependency; 848 | productName = Models; 849 | }; 850 | 22722D532CE3314A0045DB78 /* Models */ = { 851 | isa = XCSwiftPackageProductDependency; 852 | productName = Models; 853 | }; 854 | /* End XCSwiftPackageProductDependency section */ 855 | }; 856 | rootObject = 22DE8DD32C74B36800FC6EEA /* Project object */; 857 | } 858 | --------------------------------------------------------------------------------